• 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
11class IfDefMismatchPreprocessorReader < PreprocessorReader
12  attr_reader :found_conditionals
13  attr_reader :warned
14
15  class CursorWithAttributes
16    attr_reader :cursor
17    attr_reader :attributes
18    attr_reader :line
19
20    def initialize cursor, attributes, line
21      @cursor, @attributes, @line = cursor, attributes, line
22    end
23  end
24
25  def initialize document, lines
26    @found_conditionals = Array.new
27    super(document,lines)
28  end
29
30  def is_adoc_begin_conditional line
31    line.start_with?( 'ifdef::', 'ifndef::' ) && line.end_with?('[]')
32  end
33
34  def is_adoc_begin_conditional_eval line
35    line.start_with?( 'ifeval::[' ) && line.end_with?(']')
36  end
37
38  def is_adoc_end_conditional line
39    line.start_with?( 'endif::' ) && line.end_with?('[]')
40  end
41
42  def conditional_attributes line
43    line.delete_prefix('ifdef::').delete_prefix('ifndef::').delete_prefix('endif::').delete_suffix('[]')
44  end
45
46  def process_line line
47    new_line = line
48    if is_adoc_begin_conditional(line)
49      # Standard conditionals, add the conditional to a stack to be unwound as endifs are found
50      @found_conditionals.push CursorWithAttributes.new cursor, conditional_attributes(line), line
51    elsif is_adoc_begin_conditional_eval(line)
52      # ifeval conditionals do not have attributes, so store those slightly differently
53      @found_conditionals.push CursorWithAttributes.new cursor, '', line
54    elsif is_adoc_end_conditional(line)
55      # Try to match each endif to a previously defined conditional, logging errors as it goes
56      match_found = false
57      pop_count = 0
58      error_stack = Array.new
59      @found_conditionals.reverse_each do |conditional|
60        # Try the whole stack to find a match in case there is an extra ifdef in the way
61        pop_count += 1
62        if conditional.attributes == conditional_attributes(line)
63          match_found = true
64          break
65        end
66      end
67
68      if match_found
69        # First pop any non-matching conditionals and fire a mismatch error
70        (pop_count - 1).times do
71          # Warn about fixing preprocessor directives before any other issue, as these often cause a domino effect
72          if not @warned
73            logger.warn "Preprocessor conditional mismatch detected - these should be addressed before attempting to fix any other errors."
74            @warned = true
75          end
76
77          # Log an error
78          conditional = @found_conditionals.pop
79          logger.error message_with_context %(unmatched conditional "#{conditional.line}" with no endif), source_location: conditional.cursor
80
81          # Insert an endif statement so asciidoctor's default reader does not throw extraneous mismatch errors.
82          # This can mess with the way blocks are terminated, but errors will only be thrown if they would have been thrown without this checker; they will just be different.
83          #
84          # e.g.:
85          # [source,c]
86          # ----
87          # ifdef::undefined_attribute[]
88          # Some text
89          # ifdef::undefined_attribute[] // should be an endif
90          # ----                         // left unparsed because the ifdef is open
91          #                              // Script adds 2 'endif::undefined_attribute[]' lines here
92          # endif::another_attribute[]   // Irrelevant whether this is defined or not
93          #
94          # Ideally these errors would be suppressed too, but that requires a lot more complexity; e.g. rewinding the reader back to the ifdef and removing it
95          extra_line = %(endif::#{conditional.attributes}[])
96          unshift(extra_line)
97          super(extra_line)
98        end
99
100        # Pop the matching conditional
101        @found_conditionals.pop
102      else
103        # Warn about fixing preprocessor directives before any other issue, as these often cause a domino effect
104        if not @warned
105          logger.warn "Preprocessor conditional mismatch detected - these should be addressed before attempting to fix any other errors."
106          @warned = true
107        end
108
109        # If no match was found, then this is an orphaned endif
110        logger.error message_with_context %(unmatched endif - found "#{line}" with no matching conditional begin), source_location: cursor
111
112        # Hide the endif so that asciidoctor's default reader does not try to match it anyway
113        new_line = ''
114      end
115    end
116
117    super(new_line)
118  end
119end
120
121# Preprocessor hook to iterate over ifdefs to prevent them from affecting asciidoctor's processing.
122class IfDefMismatchPreprocessor < Extensions::Preprocessor
123  def process document, reader
124    # Create a new reader to return which raises errors for mismatched conditionals
125    reader = IfDefMismatchPreprocessorReader.new(document, reader.lines)
126  end
127end
128
129end
130