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