_plugins/jekyll_relative_links.rb (193 lines of code) (raw):

# taken from https://github.com/benbalter/jekyll-relative-links/tree/master/lib/jekyll-relative-links # normally we'd add it to Gemfile and include { plugins: [ jekyll-relative-links ] } in the config.yml # but the last released version 0.6.1 is too old and doesn't support absolute links, so we copy head here instead # additional changes are in commit history # - speculatively map .html to .md when doing the lookup # - make url_for_path public, and have way to inject site and initialize context outside of normal generator usage # distributed under the MIT License as follows (note this is only used to build the docs, not included with any Brooklyn output): # MIT License # # Copyright (c) 2016 Ben Balter # # 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. module JekyllRelativeLinks class Context attr_reader :site def initialize(site) @site = site end def registers { :site => site } end end class Generator < Jekyll::Generator attr_accessor :site, :config # Use Jekyll's native relative_url filter include Jekyll::Filters::URLFilters LINK_TEXT_REGEX = %r!(.*?)!.freeze FRAGMENT_REGEX = %r!(#.+?|)?!.freeze TITLE_REGEX = %r{(\s+"(?:\\"|[^"])*(?<!\\)"|\s+"(?:\\'|[^'])*(?<!\\)')?}.freeze FRAG_AND_TITLE_REGEX = %r!#{FRAGMENT_REGEX}#{TITLE_REGEX}!.freeze INLINE_LINK_REGEX = %r!\[#{LINK_TEXT_REGEX}\]\(([^\)]*?)#{FRAG_AND_TITLE_REGEX}\)!.freeze REFERENCE_LINK_REGEX = %r!^\s*?\[#{LINK_TEXT_REGEX}\]: (.+?)#{FRAG_AND_TITLE_REGEX}\s*?$!.freeze LINK_REGEX = %r!(#{INLINE_LINK_REGEX}|#{REFERENCE_LINK_REGEX})!.freeze CONVERTER_CLASS = Jekyll::Converters::Markdown CONFIG_KEY = "relative_links" ENABLED_KEY = "enabled" COLLECTIONS_KEY = "collections" LOG_KEY = "Relative Links:" safe true priority :lowest # warnings can be misleading e.g. if the link is in an excluded liquid tag; but might be useful in places # for most things, use eg htmlproofer on the output site @warn = false def initialize(config) @config = config end def prepare_for_site(site) @potential_targets = nil @site = site @context = context end def warn_missing_links(warn) @warn = warn end def generate(site) return if disabled? @site = site @context = context documents = site.pages documents = site.pages + site.docs_to_write if collections? documents.each do |document| next unless markdown_extension?(document.extname) next if document.is_a?(Jekyll::StaticFile) next if excluded?(document) replace_relative_links!(document) end end def replace_relative_links!(document) return document if document.content.nil? document.content = replace_relative_links_in_content(document.content, document.relative_path) replace_relative_links_excerpt!(document) rescue ArgumentError => e raise e unless e.to_s.start_with?("invalid byte sequence in UTF-8") end def replace_relative_links_in_content(content, relative_to_path) url_base = File.dirname(relative_to_path) content.dup.gsub(LINK_REGEX) do |original| link = link_parts(Regexp.last_match) if (link.path == "" && link.fragment == "" && link.text && link.text.start_with?("http")) link.path = link.text else next original unless replaceable_link?(link.path) path = path_from_root(link.path, url_base) url = url_for_path(path, relative_to_path) next original unless url link.path = url end replacement_text(link) end end def url_for_path_absolute(path) is_absolute = path.start_with? "/" is_section = path.include? "#" fragment = path.sub(/[^#]*#/, "") if is_section path = path.sub(/#.*/,"") if is_section path = path.sub(%r!\A/!, "") # puts "lookup #{path} / #{path.sub(%r!\.html!.freeze, ".md")}" url = url_for_path_internal(path) # also try html and / mapped to md - useful if using a baseurl url = url_for_path_internal(path.sub(%r!\.html!.freeze, ".md")) unless url url = url_for_path_internal(path.sub(%r!\.md!.freeze, ".html")) unless url url = url_for_path_internal(path.sub(%r!/\z!.freeze, "") + "/index.md") unless url url = url_for_path_internal(path.sub(%r!/\z!.freeze, "") + "/index.html") unless url url = "/" + url if url && is_absolute && !url.start_with?("/") url = "#{url}##{fragment}" if is_section url end private def url_for_path(path, src) path.sub!(%r!\A/!, "") # puts "lookup #{path} / #{path.sub(%r!\.html!.freeze, ".md")}" url = url_for_path_internal(path) pathWithText = %r!^(.*\.(png|jpg|gif|svg))( .*)\z!.freeze.match(path) # don't match images if (!pathWithText) # try to find it with .html suffix or if path to folder with or without / or if extension omitted url = url_for_path_internal(path.sub(%r!\.html!.freeze, ".md")) unless url url = url_for_path_internal(path.sub(%r!/\z!.freeze, "") + "/index.md") unless url url = url_for_path_internal(path.sub(%r!\z!.freeze, "") + "/index.md") unless url url = url_for_path_internal(path.sub(%r!\z!.freeze, ".md")) unless url puts "WARN: unresolved link in #{src}: #{path}" unless url if @warn else url = url_for_path(pathWithText[1], src) if pathWithText url = url + pathWithText[3] if url url = path unless url end url end def url_for_path_internal(path) path = path.sub(%r!\A/!, "") path = CGI.unescape(path) target = potential_targets.find { |p| p.relative_path.sub(%r!\A/!, "") == path } relative_url(target.url) if target&.url end # Stores info on a Markdown Link (avoid rubocop's Metrics/ParameterLists warning) Link = Struct.new(:link_type, :text, :path, :fragment, :title) def link_parts(matches) last_inline = 5 link_type = matches[2] ? :inline : :reference link_text = matches[link_type == :inline ? 2 : last_inline + 1] relative_path = matches[link_type == :inline ? 3 : last_inline + 2] fragment = matches[link_type == :inline ? 4 : last_inline + 3] title = matches[link_type == :inline ? 5 : last_inline + 4] Link.new(link_type, link_text, relative_path, fragment, title) end def context @context ||= JekyllRelativeLinks::Context.new(site) end def markdown_extension?(extension) markdown_converter.matches(extension) end def markdown_converter @markdown_converter ||= site.find_converter_instance(CONVERTER_CLASS) end def potential_targets @potential_targets ||= site.pages + site.static_files + site.docs_to_write end def path_from_root(relative_path, url_base) is_absolute = relative_path.start_with? "/" relative_path.sub!(%r!\A/!, "") base = is_absolute ? "" : url_base absolute_path = File.expand_path(relative_path, base) absolute_path.sub(%r!\A#{Regexp.escape(Dir.pwd)}/!, "") end # @param link [Link] A Link object describing the markdown link to make def replacement_text(link) link.path << link.fragment if link.fragment if link.link_type == :inline "[#{link.text}](#{link.path}#{link.title})" else "\n[#{link.text}]: #{link.path}#{link.title}" end end def absolute_url?(string) return unless string Addressable::URI.parse(string).absolute? rescue Addressable::URI::InvalidURIError nil end def fragment?(string) string&.start_with?("#") end def replaceable_link?(string) !fragment?(string) && !absolute_url?(string) && string != "" end def option(key) config[CONFIG_KEY] && config[CONFIG_KEY][key] end def disabled? option(ENABLED_KEY) == false end def collections? option(COLLECTIONS_KEY) == true end def excluded?(document) return false unless option("exclude") entry_filter = if document.respond_to?(:collection) document.collection.entry_filter else global_entry_filter end entry_filter.glob_include?(option("exclude"), document.relative_path).tap do |excluded| Jekyll.logger.debug(LOG_KEY, "excluded #{document.relative_path}") if excluded end end def global_entry_filter @global_entry_filter ||= Jekyll::EntryFilter.new(site) end def replace_relative_links_excerpt!(document) document.data["excerpt"] = Jekyll::Excerpt.new(document) if document.data["excerpt"] end end end