• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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