├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── assets └── logo.png ├── jekyll-target-blank.gemspec ├── lib ├── jekyll-target-blank.rb └── jekyll-target-blank │ └── version.rb ├── scripts ├── cibuild ├── quality └── test └── spec ├── fixtures └── unit │ ├── _config.yml │ ├── _docs │ ├── document-with-a-processable-link.md │ ├── document-with-include.md │ ├── document-with-liquid-tag.md │ └── test-file.txt │ ├── _includes │ └── include.html │ ├── _layouts │ └── default.html │ ├── _posts │ ├── 2018-05-17-post-with-plain-text-link.md │ ├── 2018-05-19-post-with-html-anchor-tag.md │ ├── 2018-05-20-post-with-external-markdown-link.md │ ├── 2018-05-21-post-with-relative-markdown-link.md │ ├── 2018-05-22-post-with-multiple-external-markdown-links.md │ ├── 2018-05-23-post-with-absolute-internal-markdown-link.md │ ├── 2018-05-24-post-with-code-block.md │ ├── 2018-05-30-post-with-mailto-link.md │ ├── 2018-07-02-post-with-external-html-link-and-random-css-classes.md │ ├── 2018-07-02-post-with-html-link-containing-the-specified-css-class.md │ └── 2018-07-05-post-with-external-link-containing-the-specified-css-class-and-other-css-classes.md │ └── index.md ├── jekyll-target_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /*.gem 3 | Gemfile.lock 4 | dev_notes.md 5 | .vscode 6 | spec/fixtures/unit/.jekyll-cache 7 | pkg 8 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --order random 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | 2 | 3 | AllCops: 4 | TargetRubyVersion: 2.6 5 | Exclude: 6 | - vendor/**/* 7 | 8 | Layout/LineLength: 9 | Exclude: 10 | - spec/**/* 11 | - jekyll-target-blank.gemspec 12 | 13 | Metrics/BlockLength: 14 | Exclude: 15 | - spec/**/* 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.5 4 | - 2.4 5 | - 2.6 6 | before_install: 7 | - gem update --system 8 | - gem install bundler 9 | script: scripts/cibuild 10 | sudo: false 11 | cache: bundler 12 | env: 13 | global: 14 | - NOKOGIRI_USE_SYSTEM_LIBRARIES=true 15 | notifications: 16 | irc: 17 | on_success: change 18 | on_failure: change 19 | channels: 20 | - irc.freenode.org#jekyll 21 | template: 22 | - '%{repository}#%{build_number} (%{branch}) %{message} %{build_url}' 23 | email: 24 | on_success: never 25 | on_failure: never 26 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Keith Mifsud and approved contributors. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jekyll Target Blank 2 | 3 | ![Jekyll Target Blank Logo](assets/logo.png "Jekyll Target Blank") 4 | 5 | Automatically adds a `target="_blank" rel="noopener noreferrer"` attribute to all __external__ links in Jekyll's content plus several other automation features for the external links. [Read more here](https://keith-mifsud.me/projects/jekyll-target-blank) 6 | 7 | [![Gem Version](https://badge.fury.io/rb/jekyll-target-blank.svg)](https://badge.fury.io/rb/jekyll-target-blank) 8 | [![Build Status](https://travis-ci.org/keithmifsud/jekyll-target-blank.svg?branch=master)](https://travis-ci.org/keithmifsud/jekyll-target-blank) 9 | 10 | ## Installation 11 | 12 | Add the following to your site's `Gemfile` 13 | 14 | ``` 15 | gem 'jekyll-target-blank' 16 | ``` 17 | 18 | and add the following to your site's `_config.yml` 19 | 20 | ```yml 21 | plugins: 22 | - jekyll-target-blank 23 | ``` 24 | 25 | Note: if `jekyll --version` is less than `3.5` use: 26 | 27 | ```yml 28 | gems: 29 | - jekyll-target-blank 30 | ``` 31 | 32 | ## Usage 33 | 34 | By default. all anchor tags and markdown links pointing to an external host, other than the one listed as the `url` in Jekyll's `_config.yml` will automatically be opened in a new browser tab once the site is generated. 35 | 36 | All the links in pages, posts and custom collections are included except for __plain text links. 37 | 38 | ### Examples 39 | 40 | #### HTML 41 | 42 | The following `HTML` anchor tag: 43 | 44 | ```html 45 | Google 46 | ``` 47 | 48 | will be replaced with: 49 | 50 | ```html 51 | Google 52 | ``` 53 | 54 | ..unless your website's URL is google.com 😉 55 | 56 | #### Markdown 57 | 58 | ```markdown 59 | [Google](https://google.com) 60 | ``` 61 | 62 | will be generated as: 63 | 64 | ```html 65 | Google 66 | ``` 67 | 68 | ### Configuration 69 | 70 | No custom configuration is needed for using this plugin, however, you can override some default behaviours and also make use of some extra features as explained in this section. 71 | 72 | #### Override the default behaviour 73 | 74 | You can override the default behaviour and only force external links to open in new browser if they have a CSS class name included with the same value as the one listed in the Jekyll `_config.yml` file. 75 | 76 | To override this automation, add an entry in your site's `config.yml` file, specifying which CSS class name a link must have for it to be forced to open in a new browser: 77 | 78 | ```yaml 79 | target-blank: 80 | css_class: ext-link 81 | ``` 82 | 83 | With the above setting, only links containing the `class="ext-link"` attribute will be forced to open in a new browser. 84 | 85 | #### Automatically add additional CSS Classes 86 | 87 | You can also automatically add additional CSS classes to qualifying external links. This feature is useful when you want to add CSS styling to external links such as automatically displaying an icon to show the reader that the link will open in a new browser. 88 | 89 | You can add one or more __space__ separated CSS classes in `_config.yml` like so: 90 | 91 | ```yaml 92 | target-blank: 93 | add_css_classes: css-class-one css-class-two 94 | ``` 95 | 96 | The above example will add `class="css-class-one css-class-two"` to the generated anchor tag. These CSS class names will be added in addition to any other existing CSS class names of a link. 97 | 98 | #### Override the default rel attributes 99 | 100 | For security reasons, `rel="noopener noreferrer"` are added by default to all the processed external links. You can override adding any of the `noopener` and `noreferrer` values with the following entries in your site's `_config.yml` file. 101 | 102 | __To exclude the `noopener` value:__ 103 | 104 | ```yaml 105 | target-blank: 106 | noopener: false 107 | ``` 108 | 109 | __To exclude the `noreferrer` value:__ 110 | 111 | ```yaml 112 | target-blank: 113 | noreferrer: false 114 | ``` 115 | 116 | __To exclude both `noopener` and `noreferrer` values:__ 117 | 118 | ```yaml 119 | target-blank: 120 | noopener: false 121 | noreferrer: false 122 | ``` 123 | 124 | #### Adding additional rel attribute values 125 | 126 | You can add additional `rel=""` attribute values by simply specifying them in your site's `_config.yml` file. 127 | 128 | ```yaml 129 | target-blank: 130 | rel: nofollow 131 | ``` 132 | 133 | or even more than one extra: 134 | 135 | ```yaml 136 | target-blank: 137 | rel: nofollow 138 | ``` 139 | 140 | __Note:__ 141 | 142 | 143 | The `rel` setting overrides other default `rel` attribute values. Therefore, (for example), if you exclude the `noopener` value and then add it to the `rel` property, it will still be added. The following `config`: 144 | 145 | ```yaml 146 | target-blank: 147 | noopener: false 148 | rel: noopener 149 | ``` 150 | 151 | will output: 152 | 153 | ```html 154 | Some link 155 | ``` 156 | 157 | 158 | ## Support 159 | 160 | Simply [create an issue](https://github.com/keithmifsud/jekyll-target-blank/issues/new) and I will respond as soon as possible. 161 | 162 | 163 | ## Contributing 164 | 165 | 1. [Fork it](https://github.com/keithmifsud/jekyll-target-blank/fork) 166 | 2. Create your feature branch (`git checkout -b my-new-feature) 167 | 3. Commit your changes (`git commit -m 'Add some feature'`) 168 | 4. Push to the branch (git push origin my-new-feature) 169 | 4. Create a new Pull Request 170 | 171 | 172 | ### Testing 173 | 174 | ```bash 175 | rake spec 176 | # or 177 | rspec 178 | ``` 179 | 180 | ## Credits 181 | 182 | The logo illustration was Designed by Freepik. Thank you ❤️ 183 | 184 | 185 | ## Legal 186 | 187 | This software is distributed under the [MIT](LICENSE.md) license. 188 | 189 | © 2018 - Keith Mifsud and approved contributors. 190 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithmifsud/jekyll-target-blank/b8c7fb51f8d101203f6c0aac453196a81f3f51d7/assets/logo.png -------------------------------------------------------------------------------- /jekyll-target-blank.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'jekyll-target-blank/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'jekyll-target-blank' 9 | spec.version = JekyllTargetBlank::VERSION 10 | spec.authors = ['Keith Mifsud'] 11 | spec.email = ['mifsud.k@gmail.com'] 12 | spec.summary = 'Target Blank automatically changes the external links to open in a new browser.' 13 | spec.description = 'Target Blank automatically changes the external links to open in a new browser for Jekyll sites.' 14 | spec.homepage = 'https://github.com/keithmifsud/jekyll-target-blank' 15 | spec.license = 'MIT' 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.require_paths = ['lib'] 18 | spec.required_ruby_version = '>= 2.3.0' 19 | spec.add_dependency 'jekyll', '>= 3.0', '<5.0' 20 | spec.add_dependency 'nokogiri', '~> 1.10' 21 | spec.add_development_dependency 'bundler', '~> 2.0' 22 | spec.add_development_dependency 'rake', '~> 12.0' 23 | spec.add_development_dependency 'rspec', '~> 3.0' 24 | spec.add_development_dependency 'rubocop', '~> 1' 25 | spec.add_development_dependency "rubocop-jekyll", "~> 0.12.0" 26 | 27 | end 28 | -------------------------------------------------------------------------------- /lib/jekyll-target-blank.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "jekyll" 4 | require "nokogiri" 5 | require "uri" 6 | 7 | module Jekyll 8 | class TargetBlank 9 | BODY_START_TAG = "]*)>\s*!.freeze 11 | 12 | class << self 13 | # Public: Processes the content and updated the external links 14 | # by adding target="_blank" and rel="noopener noreferrer" attributes. 15 | # 16 | # content - the document or page to be processes. 17 | def process(content) 18 | @site_url = content.site.config["url"] 19 | @config = content.site.config 20 | @target_blank_config = class_config 21 | @requires_specified_css_class = false 22 | @required_css_class_name = nil 23 | @should_add_css_classes = false 24 | @css_classes_to_add = nil 25 | @should_add_noopener = true 26 | @should_add_noreferrrer = true 27 | @should_add_extra_rel_attribute_values = false 28 | @extra_rel_attribute_values = nil 29 | 30 | return unless content.output.include?("") 64 | 65 | processed_markup = process_anchor_tags(body_content) 66 | 67 | content.output = String.new(head) << opener << processed_markup << rest.join 68 | end 69 | 70 | # Private: Processes the anchor tags and adds the target 71 | # attribute if the link is external and depending on the config settings. 72 | # 73 | # html = the html which includes the anchor tags. 74 | def process_anchor_tags(html) 75 | content = Nokogiri::HTML::DocumentFragment.parse(html) 76 | anchors = content.css("a[href]") 77 | anchors.each do |item| 78 | if processable_link?(item) 79 | add_target_blank_attribute(item) 80 | add_rel_attributes(item) 81 | add_css_classes_if_required(item) 82 | end 83 | next 84 | end 85 | content.to_html 86 | end 87 | 88 | # Private: Determines of the link should be processed. 89 | # 90 | # link = Nokogiri node. 91 | def processable_link?(link) 92 | if not_mailto_link?(link["href"]) && external?(link["href"]) 93 | if @requires_specified_css_class 94 | return false unless includes_specified_css_class?(link) 95 | end 96 | true 97 | end 98 | end 99 | 100 | # Private: Handles adding the target attribute of the config 101 | # requires a specifies class. 102 | def requires_css_class_name 103 | if css_class_name_specified_in_config? 104 | @requires_specified_css_class = true 105 | @required_css_class_name = specified_class_name_from_config 106 | end 107 | end 108 | 109 | # Private: Configures any additional CSS classes 110 | # if needed. 111 | def configure_adding_additional_css_classes 112 | if should_add_css_classes? 113 | @should_add_css_classes = true 114 | @css_classes_to_add = css_classes_to_add_from_config.to_s 115 | end 116 | end 117 | 118 | # Private: Handles the default rel attribute values 119 | def add_default_rel_attributes? 120 | @should_add_noopener = false if should_not_include_noopener? 121 | 122 | @should_add_noreferrrer = false if should_not_include_noreferrer? 123 | end 124 | 125 | # Private: Sets any extra rel attribute values 126 | # if required. 127 | def add_extra_rel_attributes? 128 | if should_add_extra_rel_attribute_values? 129 | @should_add_extra_rel_attribute_values = true 130 | @extra_rel_attribute_values = extra_rel_attribute_values_to_add 131 | end 132 | end 133 | 134 | # Private: adds the cs classes if set in config. 135 | # 136 | # link = Nokogiri node. 137 | def add_css_classes_if_required(link) 138 | if @should_add_css_classes 139 | existing_classes = get_existing_css_classes(link) 140 | existing_classes = " " + existing_classes unless existing_classes.to_s.empty? 141 | link["class"] = @css_classes_to_add + existing_classes 142 | end 143 | end 144 | 145 | # Private: Adds a target="_blank" to the link. 146 | # 147 | # link = Nokogiri node. 148 | def add_target_blank_attribute(link) 149 | link["target"] = "_blank" 150 | end 151 | 152 | # Private: Adds the rel attribute and values to the link. 153 | # 154 | # link = Nokogiri node. 155 | def add_rel_attributes(link) 156 | rel = link["rel"] || "" 157 | rel = add_noopener_to_rel(rel) 158 | 159 | if @should_add_noreferrrer 160 | rel += " " unless rel.empty? 161 | rel += "noreferrer" 162 | end 163 | 164 | if @should_add_extra_rel_attribute_values 165 | rel += " " unless rel.empty? 166 | rel += @extra_rel_attribute_values 167 | end 168 | 169 | link["rel"] = rel unless rel.empty? 170 | end 171 | 172 | # Private: Adds noopener attribute. 173 | # 174 | # rel = string 175 | def add_noopener_to_rel(rel) 176 | if @should_add_noopener 177 | rel += " " unless rel.empty? 178 | rel += "noopener" 179 | end 180 | rel 181 | end 182 | 183 | # Private: Checks if the link is a mailto url. 184 | # 185 | # link - a url. 186 | def not_mailto_link?(link) 187 | true unless link.to_s.start_with?("mailto:") 188 | end 189 | 190 | # Private: Checks if the links points to a host 191 | # other than that set in Jekyll's configuration. 192 | # 193 | # link - a url. 194 | def external?(link) 195 | if link&.match?(URI.regexp(%w(http https))) 196 | URI.parse(link).host != URI.parse(@site_url).host 197 | end 198 | end 199 | 200 | # Private: Checks if a css class name is specified in config 201 | def css_class_name_specified_in_config? 202 | target_blank_config = @target_blank_config 203 | case target_blank_config 204 | when nil, NilClass 205 | false 206 | else 207 | target_blank_config.fetch("css_class", false) 208 | end 209 | end 210 | 211 | # Private: Checks if the link contains the same css class name 212 | # as specified in config. 213 | # 214 | # link - the url under test. 215 | def includes_specified_css_class?(link) 216 | link_classes = get_existing_css_classes(link) 217 | if link_classes 218 | link_classes = link_classes.split(" ") 219 | contained = false 220 | link_classes.each do |name| 221 | contained = true unless name != @required_css_class_name 222 | end 223 | return contained 224 | end 225 | false 226 | end 227 | 228 | # Private: Gets the the css classes of the link. 229 | # 230 | # link - an anchor tag. 231 | def get_existing_css_classes(link) 232 | link["class"].to_s 233 | end 234 | 235 | # Private: Checks if the link contains the class attribute. 236 | # 237 | # link - an anchor tag. 238 | def link_has_class_attribute?(link) 239 | link.include?("class=") 240 | end 241 | 242 | # Private: Fetches the specified css class name 243 | # from config. 244 | def specified_class_name_from_config 245 | target_blank_config = @target_blank_config 246 | target_blank_config.fetch("css_class") 247 | end 248 | 249 | # Private: Checks if it should add additional CSS classes. 250 | def should_add_css_classes? 251 | config = @target_blank_config 252 | case config 253 | when nil, NilClass 254 | false 255 | else 256 | config.fetch("add_css_classes", false) 257 | end 258 | end 259 | 260 | # Private: Checks if any addional rel attribute values 261 | # should be added. 262 | def should_add_extra_rel_attribute_values? 263 | config = @target_blank_config 264 | case config 265 | when nil, NilClass 266 | false 267 | else 268 | config.fetch("rel", false) 269 | end 270 | end 271 | 272 | # Private: Gets any additional rel attribute values 273 | # values to add from config. 274 | def extra_rel_attribute_values_to_add 275 | config = @target_blank_config 276 | config.fetch("rel") 277 | end 278 | 279 | # Private: Gets the CSS classes to be added to the link from 280 | # config. 281 | def css_classes_to_add_from_config 282 | config = @target_blank_config 283 | config.fetch("add_css_classes") 284 | end 285 | 286 | # Private: Determines if the noopener rel attribute value should be added 287 | # based on the specified config values. 288 | # 289 | # Returns true if noopener is false in config. 290 | def should_not_include_noopener? 291 | config = @target_blank_config 292 | case config 293 | when nil, NilClass 294 | false 295 | else 296 | noopener = config.fetch("noopener", true) 297 | if noopener == false 298 | return true 299 | else 300 | return false 301 | end 302 | end 303 | end 304 | 305 | # Private: Determines if the noreferrer rel attribute value should be added 306 | # based on the specified config values. 307 | # 308 | # Returns true if noreferrer is false in config. 309 | def should_not_include_noreferrer? 310 | config = @target_blank_config 311 | case config 312 | when nil, NilClass 313 | false 314 | else 315 | noreferrer = config.fetch("noreferrer", true) 316 | if noreferrer == false 317 | return true 318 | else 319 | return false 320 | end 321 | end 322 | end 323 | 324 | # Private: Gets the relative config values 325 | # if they exist. 326 | def class_config 327 | @target_blank_config = @config.fetch("target-blank", nil) 328 | end 329 | end 330 | end 331 | end 332 | 333 | # Hooks into Jekyll's post_render event. 334 | Jekyll::Hooks.register [:pages, :documents], :post_render do |doc| 335 | Jekyll::TargetBlank.process(doc) if Jekyll::TargetBlank.document_processable?(doc) 336 | end 337 | -------------------------------------------------------------------------------- /lib/jekyll-target-blank/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JekyllTargetBlank 4 | VERSION = "2.0.2" 5 | end 6 | -------------------------------------------------------------------------------- /scripts/cibuild: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | scripts/test 6 | scripts/quality 7 | bundle exec rake build 8 | -------------------------------------------------------------------------------- /scripts/quality: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Rubocop $(bundle exec rubocop --version)" 5 | bundle exec rubocop -D -E $@ 6 | success=$? 7 | if ((success != 0)); then 8 | echo -e "\nTry running \`scripts/quality -a\` to automatically fix errors" 9 | fi 10 | exit $success 11 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | bundle exec rspec "$@" 5 | -------------------------------------------------------------------------------- /spec/fixtures/unit/_config.yml: -------------------------------------------------------------------------------- 1 | url: https://keith-mifsud.me 2 | collections: 3 | docs: 4 | output: true 5 | target-blank: 6 | add_css_classes: some-class other-some-class another-some-class -------------------------------------------------------------------------------- /spec/fixtures/unit/_docs/document-with-a-processable-link.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Document with a processable link 4 | --- 5 | 6 | This is a valid [link](https://google.com). -------------------------------------------------------------------------------- /spec/fixtures/unit/_docs/document-with-include.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Document with include 3 | --- 4 | 5 | This is a document with an include: {% include include.html %} -------------------------------------------------------------------------------- /spec/fixtures/unit/_docs/document-with-liquid-tag.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Document with liquid tag 3 | --- 4 | 5 | This {{ page.path }} is a document with a liquid tag. -------------------------------------------------------------------------------- /spec/fixtures/unit/_docs/test-file.txt: -------------------------------------------------------------------------------- 1 | --- 2 | title: Text file 3 | --- 4 | 5 | Valid [link](https://google.com). -------------------------------------------------------------------------------- /spec/fixtures/unit/_includes/include.html: -------------------------------------------------------------------------------- 1 | This is an include. -------------------------------------------------------------------------------- /spec/fixtures/unit/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ page.title }} 6 | 7 | 8 | 9 | 10 |
Layout content started.
11 | {{ content }} 12 |
Layout content ended.
13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/unit/_posts/2018-05-17-post-with-plain-text-link.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Post with plain text link 4 | --- 5 | 6 | This is a plain text link to https://google.com. -------------------------------------------------------------------------------- /spec/fixtures/unit/_posts/2018-05-19-post-with-html-anchor-tag.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Post with html anchor tag 4 | --- 5 | 6 | This is an anchor tag. -------------------------------------------------------------------------------- /spec/fixtures/unit/_posts/2018-05-20-post-with-external-markdown-link.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Post with external markdown link 4 | --- 5 | 6 | Link to [Google](https://google.com). -------------------------------------------------------------------------------- /spec/fixtures/unit/_posts/2018-05-21-post-with-relative-markdown-link.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Post with relative markdown link 4 | --- 5 | 6 | Link to [contact page](/contact). -------------------------------------------------------------------------------- /spec/fixtures/unit/_posts/2018-05-22-post-with-multiple-external-markdown-links.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Post with multiple external markdown links 4 | --- 5 | 6 | This post contains three links. The first link is to [Google](https://google.com), the second link is, well, to [my website](https://keithmifsud.github.io) and since [GitHub](https://github.com) is so awesome, why not link to them too? -------------------------------------------------------------------------------- /spec/fixtures/unit/_posts/2018-05-23-post-with-absolute-internal-markdown-link.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Post with absolute internal markdown link 4 | --- 5 | 6 | This is an absolute internal [link](https://keith-mifsud.me/contact). -------------------------------------------------------------------------------- /spec/fixtures/unit/_posts/2018-05-24-post-with-code-block.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Post with code block 3 | --- 4 | 5 | Sample code: 6 | ```ruby 7 | def method(link) 8 | if link == 'https://google.com' 9 | link 10 | end 11 | end 12 | ``` 13 | 14 | Valid [link](https://google.com) -------------------------------------------------------------------------------- /spec/fixtures/unit/_posts/2018-05-30-post-with-mailto-link.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Post with mailto link 4 | --- 5 | 6 | This is a mailto link. -------------------------------------------------------------------------------- /spec/fixtures/unit/_posts/2018-07-02-post-with-external-html-link-and-random-css-classes.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Post with external html link and random css classes 4 | --- 5 | 6 | Link. -------------------------------------------------------------------------------- /spec/fixtures/unit/_posts/2018-07-02-post-with-html-link-containing-the-specified-css-class.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Post with html link containing the specified css class 4 | --- 5 | 6 | Link with the css class specified in config. -------------------------------------------------------------------------------- /spec/fixtures/unit/_posts/2018-07-05-post-with-external-link-containing-the-specified-css-class-and-other-css-classes.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Post with external link containing the specified css class and other css classes 4 | --- 5 | 6 | This is a link containing the specified css class and two other random css classes. -------------------------------------------------------------------------------- /spec/fixtures/unit/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Just a page 4 | --- 5 | 6 | This is a valid [link](https://google.com). -------------------------------------------------------------------------------- /spec/jekyll-target_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe(Jekyll::TargetBlank) do 4 | Jekyll.logger.log_level = :error 5 | 6 | let(:config_overrides) { {} } 7 | let(:config_overrides) do 8 | { 9 | 'url' => 'https://keith-mifsud.me', 10 | 'collections' => { 'docs' => { 'output' => 'true' } } 11 | } 12 | end 13 | let(:configs) do 14 | Jekyll.configuration(config_overrides.merge( 15 | 'skip_config_files' => false, 16 | 'collections' => { 'docs' => { 'output' => true } }, 17 | 'source' => unit_fixtures_dir, 18 | 'destination' => unit_fixtures_dir('_site') 19 | )) 20 | end 21 | let(:target_blank) { described_class } 22 | let(:site) { Jekyll::Site.new(configs) } 23 | let(:posts) { site.posts.docs.sort.reverse } 24 | 25 | # get some fixtures 26 | let(:post_with_external_markdown_link) { find_by_title(posts, 'Post with external markdown link') } 27 | 28 | let(:post_with_multiple_external_markdown_links) { find_by_title(posts, 'Post with multiple external markdown links') } 29 | 30 | let(:post_with_relative_markdown_link) { find_by_title(posts, 'Post with relative markdown link') } 31 | 32 | let(:post_with_absolute_internal_markdown_link) { find_by_title(posts, 'Post with absolute internal markdown link') } 33 | 34 | let(:post_with_html_anchor_tag) { find_by_title(posts, 'Post with html anchor tag') } 35 | 36 | let(:post_with_plain_text_link) { find_by_title(posts, 'Post with plain text link') } 37 | 38 | let(:document_with_a_processable_link) { find_by_title(site.collections['docs'].docs, 'Document with a processable link') } 39 | 40 | let(:text_file) { find_by_title(site.collections['docs'].docs, 'Text file') } 41 | 42 | let(:post_with_code_block) { find_by_title(posts, 'Post with code block') } 43 | let(:document_with_liquid_tag) { find_by_title(site.collections['docs'].docs, 'Document with liquid tag') } 44 | 45 | let(:document_with_include) { find_by_title(site.collections['docs'].docs, 'Document with include') } 46 | 47 | let(:post_with_mailto_link) { find_by_title(posts, 'Post with mailto link') } 48 | 49 | let(:post_with_external_html_link_and_random_css_classes) { find_by_title(posts, 'Post with external html link and random css classes') } 50 | 51 | let(:post_with_html_link_containing_the_specified_css_class) { find_by_title(posts, 'Post with html link containing the specified css class') } 52 | 53 | let(:post_with_external_link_containing_the_specified_css_class_and_other_css_classes) { find_by_title(posts, 'Post with external link containing the specified css class and other css classes') } 54 | 55 | # define common wrappers. 56 | def para(content) 57 | "

#{content}

" 58 | end 59 | 60 | before(:each) do 61 | site.reset 62 | site.read 63 | (site.pages | posts | site.docs_to_write).each { |p| p.content.strip! } 64 | site.render 65 | end 66 | 67 | context 'Without entries in config file' do 68 | let(:config_overrides) do 69 | { 'target-blank' => { 'add_css_classes' => false } } 70 | end 71 | 72 | it 'should add target attribute to external markdown link' do 73 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 74 | end 75 | 76 | it 'should add target attribute to multiple external markdown links' do 77 | expect(post_with_multiple_external_markdown_links.output).to include('

This post contains three links. The first link is to Google, the second link is, well, to my website and since GitHub is so awesome, why not link to them too?

') 78 | end 79 | 80 | it 'should not add target attribute to relative markdown link' do 81 | expect(post_with_relative_markdown_link.output).to include(para('Link to contact page.')) 82 | 83 | expect(post_with_relative_markdown_link.output).to_not include(para('Link to contact page')) 84 | end 85 | 86 | it 'should not add target attribute to absolute internal link' do 87 | expect(post_with_absolute_internal_markdown_link.output).to include('

This is an absolute internal link.

') 88 | end 89 | 90 | it 'should correctly handle existing html anchor tag' do 91 | expect(post_with_html_anchor_tag.output).to include('

This is an anchor tag.

') 92 | end 93 | 94 | it 'should not interfere with plain text link' do 95 | expect(post_with_plain_text_link.output).to include('

This is a plain text link to https://google.com.

') 96 | end 97 | 98 | it 'should process external links in collections' do 99 | expect(document_with_a_processable_link.output).to include('

This is a valid link.

') 100 | end 101 | 102 | it 'should process external links in pages' do 103 | expect(site.pages.first.output).to include('

This is a valid link.

') 104 | end 105 | 106 | it 'should not process links in non html files' do 107 | expect(text_file.output).to eq('Valid [link](https://google.com).') 108 | end 109 | 110 | it 'should not process link in code block but process link outside of block' do 111 | expect(post_with_code_block.output).to include('\'https://google.com\'') 112 | 113 | expect(post_with_code_block.output).not_to include('https://google.com') 114 | 115 | expect(post_with_code_block.output).to include('

Valid link

') 116 | end 117 | 118 | it 'should not break layouts' do 119 | expect(site.pages.first.output).to include('') 120 | expect(site.pages.first.output).to include('') 121 | end 122 | 123 | it 'should not interfere with liquid tags' do 124 | expect(document_with_liquid_tag.output).to include('

This _docs/document-with-liquid-tag.md is a document with a liquid tag.

') 125 | end 126 | 127 | it 'should not interfere with includes' do 128 | expect(document_with_include.output).to include('

This is a document with an include: This is an include.

') 129 | end 130 | 131 | it 'should not break layout content' do 132 | expect(site.pages.first.output).to include('
Layout content started.
') 133 | 134 | expect(site.pages.first.output).to include('
Layout content ended.
') 135 | end 136 | 137 | it 'should not duplicate post content' do 138 | expect(post_with_external_markdown_link.output).to eq(post_with_layout_result) 139 | end 140 | 141 | it 'should ignore mailto links' do 142 | expect(post_with_mailto_link.output).to include(para('This is a mailto link.')) 143 | end 144 | end 145 | 146 | context 'With a specified css class name' do 147 | let(:target_blank_css_class) { 'ext-link' } 148 | let(:config_overrides) do 149 | { 150 | 'target-blank' => { 151 | 'css_class' => target_blank_css_class, 152 | 'add_css_classes' => false 153 | } 154 | } 155 | end 156 | 157 | it 'should not add target attribute to external markdown link that does not have the specified css class' do 158 | expect(post_with_external_markdown_link.output).to_not include(para('Link to Google.')) 159 | end 160 | 161 | it 'should not add target attribute to external markdown link that does not have the specified css class even if it does have other css classes' do 162 | expect(post_with_external_html_link_and_random_css_classes.output).to include(para('Link.')) 163 | 164 | expect(post_with_external_html_link_and_random_css_classes.output).to_not include('target="_blank" rel="noopener noreferrer"') 165 | end 166 | 167 | it 'should add target attribute to an external link containing the specified css class' do 168 | expect(post_with_html_link_containing_the_specified_css_class.output).to include(para('Link with the css class specified in config.')) 169 | end 170 | 171 | it 'should add target attribute to an external link containing the specified css class even when other css classes are specified' do 172 | expect(post_with_external_link_containing_the_specified_css_class_and_other_css_classes.output).to include(para('This is a link containing the specified css class and two other random css classes.')) 173 | end 174 | end 175 | 176 | context 'Adds a CSS classes to the links' do 177 | let(:target_blank_add_css_class) { 'some-class' } 178 | let(:config_overrides) do 179 | { 'target-blank' => { 'add_css_classes' => target_blank_add_css_class } } 180 | end 181 | 182 | it 'should add the CSS class specified in config' do 183 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 184 | end 185 | 186 | it 'should add the CSS class specified in config even when the link already has a CSS class specified' do 187 | expect(post_with_html_link_containing_the_specified_css_class.output).to include(para('Link with the css class specified in config.')) 188 | end 189 | 190 | it 'should add the CSS class specified in config even when the link has more than CSS classes already included' do 191 | expect(post_with_external_link_containing_the_specified_css_class_and_other_css_classes.output).to include(para('This is a link containing the specified css class and two other random css classes.')) 192 | end 193 | end 194 | 195 | context 'When more than one CSS classes are specified in config' do 196 | it 'should add the CSS classes specified in config' do 197 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 198 | end 199 | 200 | it 'should add the CSS classes specified in config even when the link already has a CSS class included' do 201 | expect(post_with_html_link_containing_the_specified_css_class.output).to include(para('Link with the css class specified in config.')) 202 | end 203 | 204 | it 'should add the CSS classes specified in config even when the link already has more than one CSS classes included' do 205 | expect(post_with_external_link_containing_the_specified_css_class_and_other_css_classes.output).to include(para('This is a link containing the specified css class and two other random css classes.')) 206 | end 207 | end 208 | 209 | context 'When noopener is set to false in config' do 210 | let(:noopener) { false } 211 | let(:config_overrides) do 212 | { 213 | 'target-blank' => { 214 | 'add_css_classes' => false, 215 | 'noopener' => noopener 216 | } 217 | } 218 | end 219 | 220 | it 'should not add noopener value to the rel attribute' do 221 | expect(post_with_external_markdown_link.output).to_not include(para('Link to Google.')) 222 | end 223 | 224 | it 'should still add noreferrer value to the rel attribute' do 225 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 226 | end 227 | end 228 | 229 | context 'When noreferrer is set to false in config' do 230 | let(:noreferrer) { false } 231 | let(:config_overrides) do 232 | { 233 | 'target-blank' => { 234 | 'add_css_classes' => false, 235 | 'noreferrer' => noreferrer 236 | } 237 | } 238 | end 239 | 240 | it 'should not add noreferrer value to the rel attribute' do 241 | expect(post_with_external_markdown_link.output).to_not include(para('Link to Google.')) 242 | end 243 | 244 | it 'should still add noopener value to the rel attribute' do 245 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 246 | end 247 | end 248 | 249 | context 'When both noopener and noreferrer values are set to false in config' do 250 | let(:noopener) { false } 251 | let(:noreferrer) { false } 252 | let(:config_overrides) do 253 | { 254 | 'target-blank' => { 255 | 'add_css_classes' => false, 256 | 'noopener' => noopener, 257 | 'noreferrer' => noreferrer 258 | } 259 | } 260 | end 261 | 262 | it 'should not include the rel attribute values' do 263 | expect(post_with_external_markdown_link.output).to_not include(para('Link to Google.')) 264 | end 265 | 266 | it 'should not include the rel attribute noopener value' do 267 | expect(post_with_external_markdown_link.output).to_not include(para('Link to Google.')) 268 | end 269 | 270 | it 'should not include the rel attribute noreferrer value' do 271 | expect(post_with_external_markdown_link.output).to_not include(para('Link to Google.')) 272 | end 273 | 274 | it 'should not include any rel attributes' do 275 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 276 | end 277 | end 278 | 279 | context 'When one additional rel attribute is added in config' do 280 | let(:rel_attribute) { 'nofollow' } 281 | let(:config_overrides) do 282 | { 283 | 'target-blank' => { 284 | 'add_css_classes' => false, 285 | 'rel' => rel_attribute 286 | } 287 | } 288 | end 289 | 290 | it 'should add the extra rel attribute together with the default ones' do 291 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 292 | end 293 | end 294 | 295 | context 'When more than one additional rel attributes are added in config' do 296 | let(:rel_attribute) { 'nofollow tag' } 297 | let(:config_overrides) do 298 | { 299 | 'target-blank' => { 300 | 'add_css_classes' => false, 301 | 'rel' => rel_attribute 302 | } 303 | } 304 | end 305 | 306 | it 'should add the extra rel attributes together with the default ones' do 307 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 308 | end 309 | end 310 | 311 | context 'When one extra rel attribute value are set in config and noopener is set to false' do 312 | let(:rel_attribute) { 'nofollow' } 313 | let(:noopener) { false } 314 | let(:config_overrides) do 315 | { 316 | 'target-blank' => { 317 | 'add_css_classes' => false, 318 | 'noopener' => noopener, 319 | 'rel' => rel_attribute 320 | } 321 | } 322 | end 323 | 324 | it 'should the extra rel attribute value and not add the default noopener value' do 325 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 326 | end 327 | end 328 | 329 | context 'When more than one extra rel attribute values are set in config and noopener is set to false' do 330 | let(:rel_attribute) { 'nofollow tag' } 331 | let(:noopener) { false } 332 | let(:config_overrides) do 333 | { 334 | 'target-blank' => { 335 | 'add_css_classes' => false, 336 | 'noopener' => noopener, 337 | 'rel' => rel_attribute 338 | } 339 | } 340 | end 341 | 342 | it 'should the extra rel attribute values and not add the default noopener value' do 343 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 344 | end 345 | end 346 | 347 | context 'When one extra rel attributes is set in config and both noopener and noreferer are set to false' do 348 | let(:rel_attribute) { 'nofollow' } 349 | let(:noopener) { false } 350 | let(:noreferrer) { false } 351 | let(:config_overrides) do 352 | { 353 | 'target-blank' => { 354 | 'add_css_classes' => false, 355 | 'noopener' => noopener, 356 | 'noreferrer' => noreferrer, 357 | 'rel' => rel_attribute 358 | } 359 | } 360 | end 361 | 362 | it 'should add the extra rel attribute value and no default ones' do 363 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 364 | end 365 | end 366 | 367 | context 'When more than one extra rel attribute values are set in config and both noopener and noreferer are set to false' do 368 | let(:rel_attribute) { 'nofollow tag' } 369 | let(:noopener) { false } 370 | let(:noreferrer) { false } 371 | let(:config_overrides) do 372 | { 373 | 'target-blank' => { 374 | 'add_css_classes' => false, 375 | 'noopener' => noopener, 376 | 'noreferrer' => noreferrer, 377 | 'rel' => rel_attribute 378 | } 379 | } 380 | end 381 | 382 | it 'should add the extra rel attribute values and no default ones' do 383 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 384 | end 385 | end 386 | 387 | context 'When noopener is set to false in config but added to the rel config property' do 388 | let(:rel_attribute) { 'noopener' } 389 | let(:noopener) { false } 390 | let(:config_overrides) do 391 | { 392 | 'target-blank' => { 393 | 'add_css_classes' => false, 394 | 'noopener' => noopener, 395 | 'rel' => rel_attribute 396 | } 397 | } 398 | end 399 | 400 | it 'should still include the noopener rel attribute value' do 401 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 402 | end 403 | end 404 | 405 | context 'When noopener is set to false in config but added t0 the rel config property alongside one more extra rel attribute value.' do 406 | let(:rel_attribute) { 'noopener nofollow' } 407 | let(:noopener) { false } 408 | let(:config_overrides) do 409 | { 410 | 'target-blank' => { 411 | 'add_css_classes' => false, 412 | 'noopener' => noopener, 413 | 'rel' => rel_attribute 414 | } 415 | } 416 | end 417 | 418 | it 'should still include the noopener rel attribute value along the extra one' do 419 | expect(post_with_external_markdown_link.output).to include(para('Link to Google.')) 420 | end 421 | end 422 | 423 | private 424 | 425 | def post_with_layout_result 426 | <<~RESULT 427 | 428 | 429 | 430 | 431 | Post with external markdown link 432 | 433 | 434 | 435 | 436 |
Layout content started.
437 |

Link to Google.

438 | 439 |
Layout content ended.
440 | 441 | 442 | RESULT 443 | end 444 | end 445 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path('../lib/jekyll-target-blank.rb', __dir__) 4 | 5 | RSpec.configure do |config| 6 | UNIT_FIXTURES_DIR = File.expand_path('fixtures/unit', __dir__) 7 | 8 | INTEGRATION_FIXTURES_DIR = File.expand_path('fixtures/integration', __dir__) 9 | 10 | def unit_fixtures_dir(*paths) 11 | File.join(UNIT_FIXTURES_DIR, *paths) 12 | end 13 | 14 | def integration_fixtures_dir(*paths) 15 | File.join(INTEGRATION_FIXTURES_DIR, *paths) 16 | end 17 | 18 | def find_by_title(docs, title) 19 | docs.find { |d| d.data['title'] == title } 20 | end 21 | 22 | # rspec-expectations config goes here. You can use an alternate 23 | # assertion/expectation library such as wrong or the stdlib/minitest 24 | # assertions if you prefer. 25 | config.expect_with :rspec do |expectations| 26 | # This option will default to `true` in RSpec 4. It makes the `description` 27 | # and `failure_message` of custom matchers include text for helper methods 28 | # defined using `chain`, e.g.: 29 | # be_bigger_than(2).and_smaller_than(4).description 30 | # # => "be bigger than 2 and smaller than 4" 31 | # ...rather than: 32 | # # => "be bigger than 2" 33 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 34 | end 35 | 36 | # rspec-mocks config goes here. You can use an alternate test double 37 | # library (such as bogus or mocha) by changing the `mock_with` option here. 38 | config.mock_with :rspec do |mocks| 39 | # Prevents you from mocking or stubbing a method that does not exist on 40 | # a real object. This is generally recommended, and will default to 41 | # `true` in RSpec 4. 42 | mocks.verify_partial_doubles = true 43 | end 44 | 45 | # These two settings work together to allow you to limit a spec run 46 | # to individual examples or groups you care about by tagging them with 47 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 48 | # get run. 49 | config.filter_run :focus 50 | config.run_all_when_everything_filtered = true 51 | 52 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 53 | # For more details, see: 54 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 55 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 56 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 57 | config.disable_monkey_patching! 58 | 59 | # This setting enables warnings. It's recommended, but in some cases may 60 | # be too noisy due to issues in dependencies. 61 | config.warnings = false 62 | 63 | # Print the 10 slowest examples and example groups at the 64 | # end of the spec run, to help surface which specs are running 65 | # particularly slow. 66 | # config.profile_examples = 10 67 | 68 | # Run specs in random order to surface order dependencies. If you find an 69 | # order dependency and want to debug it, you can fix the order by providing 70 | # the seed, which is printed after each run. 71 | # --seed 1234 72 | config.order = :random 73 | 74 | # Seed global randomization in this process using the `--seed` CLI option. 75 | # Setting this allows you to use `--seed` to deterministically reproduce 76 | # test failures related to randomization by passing the same `--seed` value 77 | # as the one that triggered the failure. 78 | Kernel.srand config.seed 79 | end 80 | --------------------------------------------------------------------------------