1# Copyright 2016-2021 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 ValidUsageToJsonPreprocessorReader < PreprocessorReader 12 def process_line line 13 if line.start_with?( 'ifdef::VK_', 'ifndef::VK_', 'endif::VK_') 14 # Turn extension ifdefs into list items for when we're processing VU later. 15 return super('* ' + line) 16 else 17 return super(line) 18 end 19 end 20end 21 22# Preprocessor hook to iterate over ifdefs to prevent them from affecting asciidoctor's processing. 23class ValidUsageToJsonPreprocessor < Extensions::Preprocessor 24 25 def process document, reader 26 # Create a new reader to return, which handles turning the extension ifdefs into something else. 27 extension_preprocessor_reader = ValidUsageToJsonPreprocessorReader.new(document, reader.lines) 28 29 detected_vuid_list = [] 30 extension_stack = [] 31 in_validusage = :outside 32 33 # Despite replacing lines in the overridden preprocessor reader, a 34 # FIXME in Reader#peek_line suggests that this doesn't work, the new lines are simply discarded. 35 # So we just run over the new lines and do the replacement again. 36 new_lines = extension_preprocessor_reader.read_lines().flat_map do | line | 37 38 # Track whether we're in a VU block or not 39 if line.start_with?(".Valid Usage") 40 in_validusage = :about_to_enter # About to enter VU 41 elsif in_validusage == :about_to_enter and line == '****' 42 in_validusage = :inside # Entered VU block 43 extension_stack.each 44 elsif in_validusage == :inside and line == '****' 45 in_validusage = :outside # Exited VU block 46 end 47 48 # Track extensions outside of the VU 49 if in_validusage == :outside and line.start_with?( 'ifdef::VK_', 'ifndef::VK_') and line.end_with?( '[]') 50 extension_stack.push line 51 elsif in_validusage == :outside and line.start_with?( 'endif::VK_') 52 extension_stack.pop 53 end 54 55 if in_validusage == :inside and line == '****' 56 # Write out the extension stack as bullets after this line 57 returned_lines = [line] 58 extension_stack.each do | extension | 59 returned_lines << ('* ' + extension) 60 # Add extra blank line to avoid this item absorbing any markup such as attributes on the next line 61 returned_lines << '' 62 end 63 returned_lines 64 elsif in_validusage == :inside and line.start_with?( 'ifdef::VK_', 'ifndef::VK_', 'endif::VK_') and line.end_with?('[]') 65 # Turn extension ifdefs into list items for when we're processing VU later. 66 ['* ' + line] 67 elsif in_validusage == :outside and line.start_with?( 'ifdef::VK_', 'ifndef::VK_', 'endif::VK_') and line.end_with?('[]') 68 # Remove the extension defines from the new lines, as we've dealt with them 69 [] 70 elsif line.match(/\[\[(VUID-([^-]+)-[^\]]+)\]\]/) 71 # Add all the VUIDs into an array to guarantee they're all caught later. 72 detected_vuid_list << line.match(/(VUID-([^-]+)-[^\]]+)/)[0] 73 [line] 74 else 75 [line] 76 end 77 end 78 79 # Stash the detected vuids into a document attribute 80 document.set_attribute('detected_vuid_list', detected_vuid_list.join("\n")) 81 82 # Return a new reader after preprocessing 83 Reader.new(new_lines) 84 end 85end 86 87require 'json' 88class ValidUsageToJsonTreeprocessor < Extensions::Treeprocessor 89 def process document 90 map = {} 91 92 # Get the global vuid list 93 detected_vuid_list = document.attr('detected_vuid_list').split("\n") 94 95 map['version info'] = { 96 'schema version' => 2, 97 'api version' => document.attr('revnumber'), 98 'comment' => document.attr('revremark'), 99 'date' => document.attr('revdate') 100 } 101 102 map['validation'] = {} 103 104 error_found = false 105 106 # Need to find all valid usage blocks within a structure or function ref page section 107 108 # This is a list of all refpage types that may contain VUs 109 vu_refpage_types = [ 110 'builtins', 111 'funcpointers', 112 'protos', 113 'spirv', 114 'structs', 115 ] 116 117 # Find all the open blocks 118 (document.find_by context: :open).each do |openblock| 119 # Filter out anything that's not a refpage 120 if openblock.attributes['refpage'] 121 if vu_refpage_types.include? openblock.attributes['type'] 122 parent = openblock.attributes['refpage'] 123 # Find all the sidebars 124 (openblock.find_by context: :sidebar).each do |sidebar| 125 # Filter only the valid usage sidebars 126 if sidebar.title == "Valid Usage" || sidebar.title == "Valid Usage (Implicit)" 127 extensions = [] 128 # There should be only one block - but just in case... 129 sidebar.blocks.each do |list| 130 # Iterate through all the items in the block, tracking which extensions are enabled/disabled. 131 132 attribute_replacements = list.attributes[:attribute_entries] 133 134 list.blocks.each do |item| 135 if item.text.start_with?('ifdef::VK_') 136 extensions << '(' + item.text[('ifdef::'.length)..-3] + ')' # Look for "ifdef" directives and add them to the list of extensions 137 elsif item.text.start_with?('ifndef::VK_') 138 extensions << '!(' + item.text[('ifndef::'.length)..-3] + ')' # Ditto for "ifndef" directives 139 elsif item.text.start_with?('endif::VK_') 140 extensions.slice!(-1) # Remove the last element when encountering an endif 141 else 142 item_text = item.text.clone 143 144 # Replace the refpage if it's present 145 item_text.gsub!(/\{refpage\}/i, parent) 146 147 # Replace any attributes specified on the list (e.g. stageMask) 148 if attribute_replacements 149 attribute_replacements.each do |replacement| 150 replacement_str = '\{' + replacement.name + '\}' 151 replacement_regex = Regexp.new(replacement_str, Regexp::IGNORECASE) 152 item_text.gsub!(replacement_regex, replacement.value) 153 end 154 end 155 156 match = nil 157 if item.text == item_text 158 # The VUID will have been converted to a href in the general case, so find that 159 match = /<a id=\"(VUID-[^"]+)\"[^>]*><\/a>(.*)/m.match(item_text) 160 else 161 # If we're doing manual attribute replacement, have to find the text of the anchor 162 match = /\[\[(VUID-[^\]]+)\]\](.*)/m.match(item_text) # Otherwise, look for the VUID. 163 end 164 165 if (match != nil) 166 vuid = match[1] 167 text = match[2].gsub("\n", ' ') # Have to forcibly remove newline characters; for some reason they're translated to the literally '\n' when converting to json. 168 169 # Delete the vuid from the detected vuid list, so we know it's been extracted successfully 170 if item.text == item_text 171 # Simple if the item text hasn't been modified 172 detected_vuid_list.delete(match[1]) 173 else 174 # If the item text has been modified, get the vuid from the unmodified text 175 detected_vuid_list.delete(/\[\[(VUID-([^-]+)-[^\]]+)\]\](.*)/m.match(item.text)[1]) 176 end 177 178 # Generate the table entry 179 entry = {'vuid' => vuid, 'text' => text} 180 181 # Initialize the database if necessary 182 if map['validation'][parent] == nil 183 map['validation'][parent] = {} 184 end 185 186 # Figure out the name of the section the entry will be added in 187 if extensions == [] 188 entry_section = 'core' 189 else 190 entry_section = extensions.join('+') 191 end 192 193 # Initialize the entry section if necessary 194 if map['validation'][parent][entry_section] == nil 195 map['validation'][parent][entry_section] = [] 196 end 197 198 # Check for duplicate entries 199 if map['validation'][parent][entry_section].include? entry 200 error_found = true 201 puts "VU Extraction Treeprocessor: ERROR - Valid Usage statement '#{entry}' is duplicated in the specification with VUID '#{vuid}'." 202 end 203 204 # Add the entry 205 map['validation'][parent][entry_section] << entry 206 207 else 208 puts "VU Extraction Treeprocessor: WARNING - Valid Usage statement without a VUID found: " 209 puts item_text 210 end 211 end 212 end 213 end 214 end 215 end 216 217 end 218 end 219 end 220 221 # Print out a list of VUIDs that were not extracted 222 if detected_vuid_list.length != 0 223 error_found = true 224 puts 'VU Extraction Treeprocessor: ERROR - Extraction failure' 225 puts 226 puts 'Some VUIDs were not successfully extracted from the specification.' 227 puts 'This is usually down to them appearing outside of a refpage (open)' 228 puts 'block; try checking where they are included.' 229 puts 'The following VUIDs were not extracted:' 230 detected_vuid_list.each do |vuid| 231 puts "\t * " + vuid 232 end 233 end 234 235 # Generate the json 236 json = JSON.pretty_generate(map) 237 outfile = document.attr('json_output') 238 239 # Verify the json against the schema, if the required gem is installed 240 begin 241 require 'json-schema' 242 243 # Read the schema in and validate against it 244 schema = IO.read(File.join(File.dirname(__FILE__), 'vu_schema.json')) 245 errors = JSON::Validator.fully_validate(schema, json, :errors_as_objects => true) 246 247 # Output errors if there were any 248 if errors != [] 249 error_found = true 250 puts 'VU Extraction JSON Validator: ERROR - Validation of the json schema failed' 251 puts 252 puts 'It is likely that there is an invalid or malformed entry in the specification text,' 253 puts 'see below error messages for details, and use their VUIDs and text to correlate them to their location in the specification.' 254 puts 255 256 errors.each do |error| 257 puts error.to_s 258 end 259 end 260 rescue LoadError 261 puts 'VU Extraction JSON Validator: WARNING - "json-schema" gem missing - skipping verification of json output' 262 # error handling code here 263 end 264 265 # Write the file and exit - no further processing required. 266 IO.write(outfile, json) 267 268 if (error_found) 269 exit! 1 270 end 271 exit! 0 272 end 273end 274end 275