resources/asciidoctor/lib/elastic_compat_preprocessor/extension.rb (85 lines of code) (raw):

# frozen_string_literal: true require 'asciidoctor/extensions' require_relative '../migration_log' ## # Preprocessor to turn Elastic's "wild west" formatted block extensions into # standard asciidoctor formatted extensions # # Turns # added[6.0.0-beta1] # coming[6.0.0-beta1] # deprecated[6.0.0-beta1] # Into # added::[6.0.0-beta1] # coming::[6.0.0-beta1] # deprecated::[6.0.0-beta1] # Because `::` is required by asciidoctor to invoke block macros but isn't # required by asciidoc. # # Turns # words words added[6.0.0-beta1] # words words changed[6.0.0-beta1] # words words deprecated[6.0.0-beta1] # Into # words words added:[6.0.0-beta1] # words words changed:[6.0.0-beta1] # words words deprecated:[6.0.0-beta1] # Because `:` is required by asciidoctor to invoke inline macros but isn't # required by asciidoc. # # Turns # include-tagged::foo[tag] # Into # include::elastic-include-tagged:foo[tag] # To chain into the ElasticIncludeTagged processor which is *slightly* different # than asciidoctor's built in tagging support. # # Turns # -- # :api: bulk # :request: BulkRequest # :response: BulkResponse # -- # Into # :api: bulk # :request: BulkRequest # :response: BulkResponse # Because asciidoctor clears attributes set in a block. See # https://github.com/asciidoctor/asciidoctor/issues/2993 # # Turns # ["source","sh",subs="attributes"] # -------------------------------------------- # wget https://artifacts.elastic.co//elasticsearch-{version}.zip # wget https://artifacts.elastic.co//elasticsearch-{version}.zip.sha512 # shasum -a 512 -c elasticsearch-{version}.zip.sha512 <1> # unzip elasticsearch-{version}.zip # cd elasticsearch-{version}/ <2> # -------------------------------------------- # <1> Compares the SHA of the downloaded `.zip` archive and the published # checksum, which should output `elasticsearch-{version}.zip: OK`. # <2> This directory is known as `$ES_HOME`. # # Into # ["source","sh",subs="attributes,callouts"] # -------------------------------------------- # wget https://artifacts.elastic.co/elasticsearch-{version}.zip # wget https://artifacts.elastic.co/elasticsearch-{version}.zip.sha512 # shasum -a 512 -c elasticsearch-{version}.zip.sha512 <1> # unzip elasticsearch-{version}.zip # cd elasticsearch-{version}/ <2> # -------------------------------------------- # <1> Compares the SHA of the downloaded `.zip` archive and the published # checksum, which should output `elasticsearch-{version}.zip: OK`. # <2> This directory is known as `$ES_HOME`. # Because asciidoc adds callouts to all "source" blocks. We'd *prefer* to do # this in the tree processor because it is less messy but we can't because # asciidoctor checks the `:callout` sub before giving us a chance to add it. # # Turns # ---- # foo # ------ # # Into # ---- # foo # ---- # Because Asciidoc permits these mismatches but asciidoctor does not. We'll # emit a warning because, permitted or not, they are bad style. # # With the help of ElasticCompatTreeProcessor turns # [source,js] # ---- # foo # ---- # // CONSOLE # # Into # [source,console] # ---- # foo # ---- # Because Elastic has thousands of these constructs but Asciidoctor feels # strongly that comments should not convey meaning. This is a totally # reasonable stance and we should migrate away from these comments in new # docs when it is possible. But for now we have to support the comments as # well. # class ElasticCompatPreprocessor < Asciidoctor::Extensions::Preprocessor INCLUDE_TAGGED_DIRECTIVE_RX = /^include-tagged::([^\[][^\[]*)\[(#{Asciidoctor::CC_ANY}+)?\]$/ SOURCE_WITH_SUBS_RX = /^\["source", ?"[^"]+", ?subs="(#{Asciidoctor::CC_ANY}+)"\]$/ CODE_BLOCK_RX = /^-----*$/ SNIPPET_RX = %r{^//\s*(AUTOSENSE|KIBANA|CONSOLE|SENSE:[^\n<]+)$} LEGACY_MACROS = 'added|beta|coming|deprecated|dev|experimental' LEGACY_BLOCK_MACRO_RX = /^\s*(#{LEGACY_MACROS})\[(.*)\]\s*$/ LEGACY_INLINE_MACRO_RX = /(#{LEGACY_MACROS})\[(.*)\]/ def process(_document, reader) reader.extend ReaderExtension end ## # Extensions to the Reader object that implement the conversions. module ReaderExtension def self.extended(base) base.extend MigrationLog base.instance_variable_set :@in_attribute_only_block, false base.instance_variable_set :@code_block_start, nil end ## # Replaces the Asciidoctor's built in line processing to do our conversion. def process_line(line) return line unless @process_lines if @in_attribute_only_block process_in_attribute_only_block line elsif line == '--' process_start_block line elsif (match = INCLUDE_TAGGED_DIRECTIVE_RX.match line) process_include_tagged line, match[1], match[2] else postprocess super end end ## # Handle a line if we're in attribute only block. We are basically a # passthrough in this state, just hunting for the block end. If we hit the # block end we eat the block delimiter because we ate the start delimiter # when entering into the attribute only block. def process_in_attribute_only_block(line) return line unless line == '--' @in_attribute_only_block = false line.clear end ## # Process a start block when, potentially shifting into the "attribute only" # block state if the block that is starting only contains attributes. If # we enter into that state then we eat the block delimiter to work around # a scoping difference between AsciiDoc and Asciidoctor. def process_start_block(line) lines = self.lines lines.shift lines.shift while Asciidoctor::AttributeEntryRx =~ lines[0] return line unless lines.shift == '--' @in_attribute_only_block = true line.clear end ## # Process the `include-tagged` directive. def process_include_tagged(line, target, tag) return if preprocess_include_directive( "elastic-include-tagged:#{target}", tag ) # the line was not a valid include line and we've logged a warning # about it so we should do the asciidoctor standard thing and keep # it intact. This is how we do that. @look_ahead += 1 line end ## # Process lines after they've been processed by the reader. def postprocess(line) return unless line # We can't modify frozen strings anyway *and* they never contain any # of the markers that we care about. return if line.frozen? fix_subs line fix_code_block_delimiters line # First convert the "block" version of these macros. We convert them # to block macros because they are alone on a line line.gsub!(LEGACY_BLOCK_MACRO_RX, '\1::[\2]') # Then convert the "inline" version of these macros. We convert them # to inline macros because they are *not* at the start of the line.... line.gsub!(LEGACY_INLINE_MACRO_RX, '\1:[\2]') # Transform Elastic's traditional comment based marking for # AUTOSENSE/KIBANA/CONSOLE snippets into a marker that we can pick # up during tree processing to turn the snippet into a marked up # CONSOLE snippet. Asciidoctor really doesn't recommend this sort of # thing but we have thousands of them and it'll take us some time to # stop doing it. line.gsub!(SNIPPET_RX, 'lang_override::[\1]') end def fix_subs(line) SOURCE_WITH_SUBS_RX.match(line) do |m| # AsciiDoc would automatically add `subs` to every source block but # Asciidoctor does not and we have thousands of blocks that rely on # this behavior. old_subs = m[1] line.sub! "subs=\"#{old_subs}\"", "subs=\"#{old_subs},callouts\"" \ unless old_subs.include? 'callouts' end end def fix_code_block_delimiters(line) return unless CODE_BLOCK_RX =~ line unless @code_block_start @code_block_start = line return end fix_bad_code_block_end line unless line == @code_block_start @code_block_start = nil end def fix_bad_code_block_end(line) line.replace @code_block_start migration_warn @document, cursor, 'delimiter-mismatch', "code block end doesn't match start" end end end