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