1# Copyright (c) 2016-2018 The Khronos Group Inc. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal' 16 17include ::Asciidoctor 18 19module Asciidoctor 20 21# Duplicate of "AnyListRx" defined by asciidoctor 22# Detects the start of any list item. 23# 24# NOTE we only have to check as far as the blank character because we know it means non-whitespace follows. 25HighlighterAnyListRx = /^(?:#{CG_BLANK}*(?:-|([*.\u2022])\1{0,4}|\d+\.|[a-zA-Z]\.|[IVXivx]+\))#{CG_BLANK}|#{CG_BLANK}*.*?(?::{2,4}|;;)(?:$|#{CG_BLANK})|<?\d+>#{CG_BLANK})/ 26 27class ExtensionHighlighterPreprocessorReader < PreprocessorReader 28 def initialize document, diff_extensions, data = nil, cursor = nil 29 super(document, data, cursor) 30 @status_stack = [] 31 @diff_extensions = diff_extensions 32 @tracking_target = nil 33 end 34 35 # This overrides the default preprocessor reader conditional logic such 36 # that any extensions which need highlighting and are enabled have their 37 # ifdefs left intact. 38 def preprocess_conditional_directive directive, target, delimiter, text 39 # If we're tracking a target for highlighting already, don't need to do 40 # additional processing unless we hit the end of that conditional 41 # section 42 # NOTE: This will break if for some absurd reason someone nests the same 43 # conditional inside itself. 44 if @tracking_target != nil && directive == 'endif' && @tracking_target == target.downcase 45 @tracking_target = nil 46 elsif @tracking_target 47 return super(directive, target, delimiter, text) 48 end 49 50 # If it's an ifdef or ifndef, push the directive onto a stack 51 # If it's an endif, pop the last one off. 52 # This is done to apply the next bit of logic to both the start and end 53 # of an conditional block correctly 54 status = directive 55 if directive == 'endif' 56 status = @status_stack.pop 57 else 58 @status_stack.push status 59 end 60 61 # If the status is negative, we need to still include the conditional 62 # text for the highlighter, so we replace the requirement for the 63 # extension attribute in question to be not defined with an 64 # always-undefined attribute, so that it evaluates to true when it needs 65 # to. 66 # Undefined attribute is currently just the extension with "_undefined" 67 # appended to it. 68 modified_target = target.downcase 69 if status == 'ifndef' 70 @diff_extensions.each do | extension | 71 modified_target.gsub!(extension, extension + '_undefined') 72 end 73 end 74 75 # Call the original preprocessor 76 result = super(directive, modified_target, delimiter, text) 77 78 # If any of the extensions are in the target, and the conditional text 79 # isn't flagged to be skipped, return false to prevent the preprocessor 80 # from removing the line from the processed source. 81 unless @skipping 82 @diff_extensions.each do | extension | 83 if target.downcase.include?(extension) 84 if directive != 'endif' 85 @tracking_target = target.downcase 86 end 87 return false 88 end 89 end 90 end 91 return result 92 end 93 94 # Identical to preprocess_conditional_directive, but older versions of 95 # Asciidoctor used a different name, so this is there to override the same 96 # method in older versions. 97 # This is a pure c+p job for awkward inheritance reasons (see use of 98 # the super() keyword :|) 99 # At some point, will rewrite to avoid this mess, but this fixes things 100 # for now without breaking things for anyone. 101 def preprocess_conditional_inclusion directive, target, delimiter, text 102 # If we're tracking a target for highlighting already, don't need to do 103 # additional processing unless we hit the end of that conditional 104 # section 105 # NOTE: This will break if for some absurd reason someone nests the same 106 # conditional inside itself. 107 if @tracking_target != nil && directive == 'endif' && @tracking_target == target.downcase 108 @tracking_target = nil 109 elsif @tracking_target 110 return super(directive, target, delimiter, text) 111 end 112 113 # If it's an ifdef or ifndef, push the directive onto a stack 114 # If it's an endif, pop the last one off. 115 # This is done to apply the next bit of logic to both the start and end 116 # of an conditional block correctly 117 status = directive 118 if directive == 'endif' 119 status = @status_stack.pop 120 else 121 @status_stack.push status 122 end 123 124 # If the status is negative, we need to still include the conditional 125 # text for the highlighter, so we replace the requirement for the 126 # extension attribute in question to be not defined with an 127 # always-undefined attribute, so that it evaluates to true when it needs 128 # to. 129 # Undefined attribute is currently just the extension with "_undefined" 130 # appended to it. 131 modified_target = target.downcase 132 if status == 'ifndef' 133 @diff_extensions.each do | extension | 134 modified_target.gsub!(extension, extension + '_undefined') 135 end 136 end 137 138 # Call the original preprocessor 139 result = super(directive, modified_target, delimiter, text) 140 141 # If any of the extensions are in the target, and the conditional text 142 # isn't flagged to be skipped, return false to prevent the preprocessor 143 # from removing the line from the processed source. 144 unless @skipping 145 @diff_extensions.each do | extension | 146 if target.downcase.include?(extension) 147 if directive != 'endif' 148 @tracking_target = target.downcase 149 end 150 return false 151 end 152 end 153 end 154 return result 155 end 156end 157 158class Highlighter 159 def initialize 160 @delimiter_stack = [] 161 @current_anchor = 1 162 end 163 164 def highlight_marks line, previous_line, next_line 165 if !(line.start_with? 'endif') 166 # Any intact "ifdefs" are sections added by an extension, and 167 # "ifndefs" are sections removed. 168 # Currently don't track *which* extension(s) is/are responsible for 169 # the addition or removal - though it would be possible to add it. 170 if line.start_with? 'ifdef' 171 role = 'added' 172 else # if line.start_with? 'ifndef' 173 role = 'removed' 174 end 175 176 # Create an anchor with the current anchor number 177 anchor = '[[difference' + @current_anchor.to_s + ']]' 178 179 # Figure out which markup to use based on the surrounding text 180 # This is robust enough as far as I can tell, though we may want to do 181 # something more generic later since currently it relies on the fact 182 # that if you start inside a list or paragraph, you'll end in the same 183 # list or paragraph and not cross to other blocks. 184 # In practice it *might just work* but it also might not. 185 # May need to consider what to do about this in future - maybe just 186 # use open blocks for everything? 187 highlight_delimiter = :inline 188 if (HighlighterAnyListRx.match(next_line) != nil) 189 # NOTE: There's a corner case here that should never be hit (famous last words) 190 # If a line in the middle of a paragraph begins with an asterisk and 191 # then whitespace, this will think it's a list item and use the 192 # wrong delimiter. 193 # That shouldn't be a problem in practice though, it just might look 194 # a little weird. 195 highlight_delimiter = :list 196 elsif previous_line.strip.empty? 197 highlight_delimiter = :block 198 end 199 200 # Add the delimiter to the stack for the matching 'endif' to consume 201 @delimiter_stack.push highlight_delimiter 202 203 # Add an appropriate method of delimiting the highlighted areas based 204 # on the surrounding text determined above. 205 if highlight_delimiter == :block 206 return ['', anchor, ":role: #{role}", ''] 207 elsif highlight_delimiter == :list 208 return ['', anchor, "[.#{role}]", '~~~~~~~~~~~~~~~~~~~~', ''] 209 else #if highlight_delimiter == :inline 210 return [anchor + ' [.' + role + ']##'] 211 end 212 else # if !(line.start_with? 'endif') 213 # Increment the anchor when we see a matching endif, and generate a 214 # link to the next diff section 215 @current_anchor = @current_anchor + 1 216 anchor_link = '<<difference' + @current_anchor.to_s + ', =>>>' 217 218 # Close the delimited area according to the previously determined 219 # delimiter 220 highlight_delimiter = @delimiter_stack.pop 221 if highlight_delimiter == :block 222 return [anchor_link, '', ':role:', ''] 223 elsif highlight_delimiter == :list 224 return [anchor_link, '~~~~~~~~~~~~~~~~~~~~', ''] 225 else #if highlight_delimiter == :inline 226 return [anchor_link + '##'] 227 end 228 end 229 end 230end 231 232# Preprocessor hook to iterate over ifdefs to prevent them from affecting asciidoctor's processing. 233class ExtensionHighlighterPreprocessor < Extensions::Preprocessor 234 def process document, reader 235 236 # Only attempt to highlight extensions that are also enabled - if one 237 # isn't, warn about it and skip highlighting that extension. 238 diff_extensions = document.attributes['diff_extensions'].downcase.split(' ') 239 actual_diff_extensions = [] 240 diff_extensions.each do | extension | 241 if document.attributes.has_key?(extension) 242 actual_diff_extensions << extension 243 else 244 puts 'The ' + extension + ' extension is not enabled - changes will not be highlighted.' 245 end 246 end 247 248 # Create a new reader to return, which leaves extension ifdefs that need highlighting intact beyond the preprocess step. 249 extension_preprocessor_reader = ExtensionHighlighterPreprocessorReader.new(document, actual_diff_extensions, reader.lines) 250 251 highlighter = Highlighter.new 252 new_lines = [] 253 254 # Store the old lines so we can reference them in a non-trivial fashion 255 old_lines = extension_preprocessor_reader.read_lines() 256 old_lines.each_index do | index | 257 258 # Grab the previously processed line 259 # This is used by the highlighter to figure out if the highlight will 260 # be inline, or part of a block. 261 if index > 0 262 previous_line = old_lines[index - 1] 263 else 264 previous_line = '' 265 end 266 267 # Current line to process 268 line = old_lines[index] 269 270 # Grab the next line to process 271 # This is used by the highlighter to figure out if the highlight is 272 # between list elements or not - which need special handling. 273 if index < (old_lines.length - 1) 274 next_line = old_lines[index + 1] 275 else 276 next_line = '' 277 end 278 279 # Highlight any preprocessor directives that were left intact by the 280 # custom preprocessor reader. 281 if line.start_with?( 'ifdef::', 'ifndef::', 'endif::') 282 new_lines += highlighter.highlight_marks(line, previous_line, next_line) 283 else 284 new_lines << line 285 end 286 end 287 288 # Return a new reader after preprocessing - this takes care of creating 289 # the AST from the new source. 290 Reader.new(new_lines) 291 end 292end 293 294class AddHighlighterCSS < Extensions::Postprocessor 295 HighlighterStyleCSS = [ 296 '.added {', 297 ' background-color: lime;', 298 ' border-color: green;', 299 ' padding:1px;', 300 '}', 301 '.removed {', 302 ' background-color: pink;', 303 ' border-color: red;', 304 ' padding:1px;', 305 '}', 306 '</style>'] 307 308 def process document, output 309 output.sub! '</style>', HighlighterStyleCSS.join("\n") 310 end 311end 312 313end 314