# Copyright (c) 2016-2019 The Khronos Group Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal' include ::Asciidoctor module Asciidoctor # Duplicate of "AnyListRx" defined by asciidoctor # Detects the start of any list item. # # NOTE we only have to check as far as the blank character because we know it means non-whitespace follows. HighlighterAnyListRx = /^(?:#{CG_BLANK}*(?:-|([*.\u2022])\1{0,4}|\d+\.|[a-zA-Z]\.|[IVXivx]+\))#{CG_BLANK}|#{CG_BLANK}*.*?(?::{2,4}|;;)(?:$|#{CG_BLANK})|#{CG_BLANK})/ class ExtensionHighlighterPreprocessorReader < PreprocessorReader def initialize document, diff_extensions, data = nil, cursor = nil super(document, data, cursor) @status_stack = [] @diff_extensions = diff_extensions @tracking_target = nil end # This overrides the default preprocessor reader conditional logic such # that any extensions which need highlighting and are enabled have their # ifdefs left intact. def preprocess_conditional_directive directive, target, delimiter, text # If we're tracking a target for highlighting already, don't need to do # additional processing unless we hit the end of that conditional # section # NOTE: This will break if for some absurd reason someone nests the same # conditional inside itself. if @tracking_target != nil && directive == 'endif' && @tracking_target == target.downcase @tracking_target = nil elsif @tracking_target return super(directive, target, delimiter, text) end # If it's an ifdef or ifndef, push the directive onto a stack # If it's an endif, pop the last one off. # This is done to apply the next bit of logic to both the start and end # of an conditional block correctly status = directive if directive == 'endif' status = @status_stack.pop else @status_stack.push status end # If the status is negative, we need to still include the conditional # text for the highlighter, so we replace the requirement for the # extension attribute in question to be not defined with an # always-undefined attribute, so that it evaluates to true when it needs # to. # Undefined attribute is currently just the extension with "_undefined" # appended to it. modified_target = target.downcase if status == 'ifndef' @diff_extensions.each do | extension | modified_target.gsub!(extension, extension + '_undefined') end end # Call the original preprocessor result = super(directive, modified_target, delimiter, text) # If any of the extensions are in the target, and the conditional text # isn't flagged to be skipped, return false to prevent the preprocessor # from removing the line from the processed source. unless @skipping @diff_extensions.each do | extension | if target.downcase.include?(extension) if directive != 'endif' @tracking_target = target.downcase end return false end end end return result end # Identical to preprocess_conditional_directive, but older versions of # Asciidoctor used a different name, so this is there to override the same # method in older versions. # This is a pure c+p job for awkward inheritance reasons (see use of # the super() keyword :|) # At some point, will rewrite to avoid this mess, but this fixes things # for now without breaking things for anyone. def preprocess_conditional_inclusion directive, target, delimiter, text # If we're tracking a target for highlighting already, don't need to do # additional processing unless we hit the end of that conditional # section # NOTE: This will break if for some absurd reason someone nests the same # conditional inside itself. if @tracking_target != nil && directive == 'endif' && @tracking_target == target.downcase @tracking_target = nil elsif @tracking_target return super(directive, target, delimiter, text) end # If it's an ifdef or ifndef, push the directive onto a stack # If it's an endif, pop the last one off. # This is done to apply the next bit of logic to both the start and end # of an conditional block correctly status = directive if directive == 'endif' status = @status_stack.pop else @status_stack.push status end # If the status is negative, we need to still include the conditional # text for the highlighter, so we replace the requirement for the # extension attribute in question to be not defined with an # always-undefined attribute, so that it evaluates to true when it needs # to. # Undefined attribute is currently just the extension with "_undefined" # appended to it. modified_target = target.downcase if status == 'ifndef' @diff_extensions.each do | extension | modified_target.gsub!(extension, extension + '_undefined') end end # Call the original preprocessor result = super(directive, modified_target, delimiter, text) # If any of the extensions are in the target, and the conditional text # isn't flagged to be skipped, return false to prevent the preprocessor # from removing the line from the processed source. unless @skipping @diff_extensions.each do | extension | if target.downcase.include?(extension) if directive != 'endif' @tracking_target = target.downcase end return false end end end return result end end class Highlighter def initialize @delimiter_stack = [] @current_anchor = 1 end def highlight_marks line, previous_line, next_line if !(line.start_with? 'endif') # Any intact "ifdefs" are sections added by an extension, and # "ifndefs" are sections removed. # Currently don't track *which* extension(s) is/are responsible for # the addition or removal - though it would be possible to add it. if line.start_with? 'ifdef' role = 'added' else # if line.start_with? 'ifndef' role = 'removed' end # Create an anchor with the current anchor number anchor = '[[difference' + @current_anchor.to_s + ']]' # Figure out which markup to use based on the surrounding text # This is robust enough as far as I can tell, though we may want to do # something more generic later since currently it relies on the fact # that if you start inside a list or paragraph, you'll end in the same # list or paragraph and not cross to other blocks. # In practice it *might just work* but it also might not. # May need to consider what to do about this in future - maybe just # use open blocks for everything? highlight_delimiter = :inline if (HighlighterAnyListRx.match(next_line) != nil) # NOTE: There's a corner case here that should never be hit (famous last words) # If a line in the middle of a paragraph begins with an asterisk and # then whitespace, this will think it's a list item and use the # wrong delimiter. # That shouldn't be a problem in practice though, it just might look # a little weird. highlight_delimiter = :list elsif previous_line.strip.empty? highlight_delimiter = :block end # Add the delimiter to the stack for the matching 'endif' to consume @delimiter_stack.push highlight_delimiter # Add an appropriate method of delimiting the highlighted areas based # on the surrounding text determined above. if highlight_delimiter == :block return ['', anchor, ":role: #{role}", ''] elsif highlight_delimiter == :list return ['', anchor, "[.#{role}]", '~~~~~~~~~~~~~~~~~~~~', ''] else #if highlight_delimiter == :inline return [anchor + ' [.' + role + ']##'] end else # if !(line.start_with? 'endif') # Increment the anchor when we see a matching endif, and generate a # link to the next diff section @current_anchor = @current_anchor + 1 anchor_link = '<>>' # Close the delimited area according to the previously determined # delimiter highlight_delimiter = @delimiter_stack.pop if highlight_delimiter == :block return [anchor_link, '', ':role:', ''] elsif highlight_delimiter == :list return [anchor_link, '~~~~~~~~~~~~~~~~~~~~', ''] else #if highlight_delimiter == :inline return [anchor_link + '##'] end end end end # Preprocessor hook to iterate over ifdefs to prevent them from affecting asciidoctor's processing. class ExtensionHighlighterPreprocessor < Extensions::Preprocessor def process document, reader # Only attempt to highlight extensions that are also enabled - if one # isn't, warn about it and skip highlighting that extension. diff_extensions = document.attributes['diff_extensions'].downcase.split(' ') actual_diff_extensions = [] diff_extensions.each do | extension | if document.attributes.has_key?(extension) actual_diff_extensions << extension else puts 'The ' + extension + ' extension is not enabled - changes will not be highlighted.' end end # Create a new reader to return, which leaves extension ifdefs that need highlighting intact beyond the preprocess step. extension_preprocessor_reader = ExtensionHighlighterPreprocessorReader.new(document, actual_diff_extensions, reader.lines) highlighter = Highlighter.new new_lines = [] # Store the old lines so we can reference them in a non-trivial fashion old_lines = extension_preprocessor_reader.read_lines() old_lines.each_index do | index | # Grab the previously processed line # This is used by the highlighter to figure out if the highlight will # be inline, or part of a block. if index > 0 previous_line = old_lines[index - 1] else previous_line = '' end # Current line to process line = old_lines[index] # Grab the next line to process # This is used by the highlighter to figure out if the highlight is # between list elements or not - which need special handling. if index < (old_lines.length - 1) next_line = old_lines[index + 1] else next_line = '' end # Highlight any preprocessor directives that were left intact by the # custom preprocessor reader. if line.start_with?( 'ifdef::', 'ifndef::', 'endif::') new_lines += highlighter.highlight_marks(line, previous_line, next_line) else new_lines << line end end # Return a new reader after preprocessing - this takes care of creating # the AST from the new source. Reader.new(new_lines) end end class AddHighlighterCSS < Extensions::Postprocessor HighlighterStyleCSS = [ '.added {', ' background-color: lime;', ' border-color: green;', ' padding:1px;', '}', '.removed {', ' background-color: pink;', ' border-color: red;', ' padding:1px;', '}', ''] def process document, output output.sub! '', HighlighterStyleCSS.join("\n") end end end