class Asciidoctor::PreprocessorReader
directives as each line is read off the Array of lines.
Public: Methods for retrieving lines from AsciiDoc source files, evaluating preprocessor
def exceeded_max_depth?
def exceeded_max_depth? if (abs_maxdepth = @maxdepth[:abs]) > 0 && @include_stack.size >= abs_maxdepth @maxdepth[:rel] else false end end
def include_depth
def include_depth @include_stack.size end
def include_processors?
def include_processors? if !@include_processor_extensions if @document.extensions? && @document.extensions.include_processors? @include_processor_extensions = @document.extensions.include_processors true else @include_processor_extensions = false false end else @include_processor_extensions != false end end
def initialize document, data = nil, cursor = nil
def initialize document, data = nil, cursor = nil @document = document super data, cursor, :normalize => true include_depth_default = document.attributes.fetch('max-include-depth', 64).to_i include_depth_default = 0 if include_depth_default < 0 # track both absolute depth for comparing to size of include stack and relative depth for reporting @maxdepth = {:abs => include_depth_default, :rel => include_depth_default} @include_stack = [] @includes = (document.references[:includes] ||= []) @skipping = false @conditional_stack = [] @include_processor_extensions = nil end
def peek_line direct = false
in the current include context or a parent include context.
Returns the next line of the source data as a String if there are lines remaining
one include on the stack.
stack if the last line has been reached and there's at least
Public: Override the Reader#peek_line method to pop the include
def peek_line direct = false if (line = super) line elsif @include_stack.empty? nil else pop_include peek_line direct end end
def pop_include
def pop_include if @include_stack.size > 0 @lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines = @include_stack.pop # FIXME kind of a hack #Document::AttributeEntry.new('infile', @file).save_to_next_block @document #Document::AttributeEntry.new('indir', ::File.dirname(@file)).save_to_next_block @document @eof = @lines.empty? @look_ahead = 0 end nil end
def prepare_lines data, opts = {}
def prepare_lines data, opts = {} result = super # QUESTION should this work for AsciiDoc table cell content? Currently it does not. if @document && (@document.attributes.has_key? 'skip-front-matter') if (front_matter = skip_front_matter! result) @document.attributes['front-matter'] = front_matter * EOL end end if opts.fetch :condense, true result.shift && @lineno += 1 while (first = result[0]) && first.empty? result.pop while (last = result[-1]) && last.empty? end if (indent = opts.fetch(:indent, nil)) Parser.reset_block_indent! result, indent.to_i end result end
def preprocess_conditional_inclusion directive, target, delimiter, text
ifndef directives, and for the conditional expression for the ifeval directive.
Used for a single-line conditional block in the case of the ifdef or
text - The text associated with this directive (occurring between the square brackets)
can be defined or undefined.
attributes must be defined or undefined, ',' means any of the attributes
delimiter - The conditional delimiter for multiple attributes ('+' means all
used in the condition (blank in the case of the ifeval directive)
target - The target, which is the name of one or more attributes that are
directive - The conditional inclusion directive (ifdef, ifndef, ifeval, endif)
available content is found.
satisfied and continue preprocessing recursively until the next line of
blocks. If the Reader is not skipping, mark whether the condition is
simply track the open and close delimiters of any nested conditional
endif) under the cursor. If the Reader is currently skipping content, then
Preprocess the conditional inclusion directive (ifdef, ifndef, ifeval,
Internal: Preprocess the directive (macro) to conditionally include content.
def preprocess_conditional_inclusion directive, target, delimiter, text # must have a target before brackets if ifdef or ifndef # must not have text between brackets if endif # don't honor match if it doesn't meet this criteria # QUESTION should we warn for these bogus declarations? if ((directive == 'ifdef' || directive == 'ifndef') && target.empty?) || (directive == 'endif' && text) return false end # attributes are case insensitive target = target.downcase if directive == 'endif' stack_size = @conditional_stack.size if stack_size > 0 pair = @conditional_stack[-1] if target.empty? || target == pair[:target] @conditional_stack.pop @skipping = @conditional_stack.empty? ? false : @conditional_stack[-1][:skipping] else warn "asciidoctor: ERROR: #{line_info}: mismatched macro: endif::#{target}[], expected endif::#{pair[:target]}[]" end else warn "asciidoctor: ERROR: #{line_info}: unmatched macro: endif::#{target}[]" end return true end skip = false unless @skipping # QUESTION any way to wrap ifdef & ifndef logic up together? case directive when 'ifdef' case delimiter when nil # if the attribute is undefined, then skip skip = !@document.attributes.has_key?(target) when ',' # if any attribute is defined, then don't skip skip = !target.split(',').detect {|name| @document.attributes.has_key? name } when '+' # if any attribute is undefined, then skip skip = target.split('+').detect {|name| !@document.attributes.has_key? name } end when 'ifndef' case delimiter when nil # if the attribute is defined, then skip skip = @document.attributes.has_key?(target) when ',' # if any attribute is undefined, then don't skip skip = !target.split(',').detect {|name| !@document.attributes.has_key? name } when '+' # if any attribute is defined, then skip skip = target.split('+').detect {|name| @document.attributes.has_key? name } end when 'ifeval' # the text in brackets must match an expression # don't honor match if it doesn't meet this criteria if !target.empty? || !(expr_match = EvalExpressionRx.match(text.strip)) return false end lhs = resolve_expr_val expr_match[1] # regex enforces a restrict set of math-related operations op = expr_match[2] rhs = resolve_expr_val expr_match[3] skip = !(lhs.send op.to_sym, rhs) end end # conditional inclusion block if directive == 'ifeval' || !text @skipping = true if skip @conditional_stack << {:target => target, :skip => skip, :skipping => @skipping} # single line conditional inclusion else unless @skipping || skip # FIXME slight hack to skip past conditional line # but keep our synthetic line marked as processed conditional_line = peek_line true replace_line text.rstrip unshift conditional_line return true end end true end
def preprocess_include raw_target, raw_attributes
target slot of the include::[] macro
target - The name of the source document to include as specified in the
If none of the above apply, emit the include directive line verbatim.
of the Array of source data.
stack size, normalize the target path and read the lines onto the beginning
Otherwise, if the max depth is greater than 0, and is not exceeded by the
attributes to that processor and expect an Array of String lines in return.
Otherwise, if an include processor is specified pass the target and
directive line is emitted verbatim.
If SafeMode is SECURE or greater, the directive is ignore and the include
are as follows:
Preprocess the directive to include the target document. The scenarios
Internal: Preprocess the directive (macro) to include the target document.
def preprocess_include raw_target, raw_attributes if (target = @document.sub_attributes raw_target, :attribute_missing => 'drop-line').empty? if @document.attributes.fetch('attribute-missing', Compliance.attribute_missing) == 'skip' replace_line %(Unresolved directive in #{@path} - include::#{raw_target}[#{raw_attributes}]) true else advance true end # assume that if an include processor is given, the developer wants # to handle when and how to process the include elsif include_processors? && (extension = @include_processor_extensions.find {|candidate| candidate.instance.handles? target }) advance # FIXME parse attributes if requested by extension extension.process_method[@document, self, target, AttributeList.new(raw_attributes).parse] true # if running in SafeMode::SECURE or greater, don't process this directive # however, be friendly and at least make it a link to the source document elsif @document.safe >= SafeMode::SECURE # FIXME we don't want to use a link macro if we are in a verbatim context replace_line %(link:#{target}[]) true elsif (abs_maxdepth = @maxdepth[:abs]) > 0 && @include_stack.size >= abs_maxdepth warn %(asciidoctor: ERROR: #{line_info}: maximum include depth of #{@maxdepth[:rel]} exceeded) false elsif abs_maxdepth > 0 if ::RUBY_ENGINE_OPAL # NOTE resolves uri relative to currently loaded document # NOTE we defer checking if file exists and catch the 404 error if it does not # TODO only use this logic if env-browser is set target_type = :file include_file = path = if @include_stack.empty? ::Dir.pwd == @document.base_dir ? target : (::File.join @dir, target) else ::File.join @dir, target end elsif target.include?(':') && UriSniffRx =~ target unless @document.attributes.has_key? 'allow-uri-read' replace_line %(link:#{target}[]) return true end target_type = :uri include_file = path = target if @document.attributes.has_key? 'cache-uri' # caching requires the open-uri-cached gem to be installed # processing will be automatically aborted if these libraries can't be opened Helpers.require_library 'open-uri/cached', 'open-uri-cached' elsif !::RUBY_ENGINE_OPAL # autoload open-uri ::OpenURI end else target_type = :file # include file is resolved relative to dir of current include, or base_dir if within original docfile include_file = @document.normalize_system_path(target, @dir, nil, :target_name => 'include file') unless ::File.file? include_file warn "asciidoctor: WARNING: #{line_info}: include file not found: #{include_file}" replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}]) return true end #path = @document.relative_path include_file path = PathResolver.new.relative_path include_file, @document.base_dir end inc_lines = nil tags = nil attributes = {} if !raw_attributes.empty? # QUESTION should we use @document.parse_attribues? attributes = AttributeList.new(raw_attributes).parse if attributes.has_key? 'lines' inc_lines = [] attributes['lines'].split(DataDelimiterRx).each do |linedef| if linedef.include?('..') from, to = linedef.split('..').map(&:to_i) if to == -1 inc_lines << from inc_lines << 1.0/0.0 else inc_lines.concat ::Range.new(from, to).to_a end else inc_lines << linedef.to_i end end inc_lines = inc_lines.sort.uniq elsif attributes.has_key? 'tag' tags = [attributes['tag']].to_set elsif attributes.has_key? 'tags' tags = attributes['tags'].split(DataDelimiterRx).uniq.to_set end end if !inc_lines.nil? if !inc_lines.empty? selected = [] inc_line_offset = 0 inc_lineno = 0 begin open(include_file, 'r') do |f| f.each_line do |l| inc_lineno += 1 take = inc_lines[0] if take.is_a?(::Float) && take.infinite? selected.push l inc_line_offset = inc_lineno if inc_line_offset == 0 else if f.lineno == take selected.push l inc_line_offset = inc_lineno if inc_line_offset == 0 inc_lines.shift end break if inc_lines.empty? end end end rescue warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}) replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}]) return true end advance # FIXME not accounting for skipped lines in reader line numbering push_include selected, include_file, path, inc_line_offset, attributes end elsif !tags.nil? if !tags.empty? selected = [] inc_line_offset = 0 inc_lineno = 0 active_tag = nil tags_found = ::Set.new begin open(include_file, 'r') do |f| f.each_line do |l| inc_lineno += 1 # must force encoding here since we're performing String operations on line l.force_encoding(::Encoding::UTF_8) if FORCE_ENCODING l = l.rstrip if active_tag if l.end_with?(%(end::#{active_tag}[])) && TagDirectiveRx =~ l active_tag = nil else selected.push l unless l.end_with?('[]') && TagDirectiveRx =~ l inc_line_offset = inc_lineno if inc_line_offset == 0 end else tags.each do |tag| if l.end_with?(%(tag::#{tag}[])) && TagDirectiveRx =~ l active_tag = tag tags_found << tag break end end end end end rescue warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}) replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}]) return true end unless (missing_tags = tags.to_a - tags_found.to_a).empty? warn "asciidoctor: WARNING: #{line_info}: tag#{missing_tags.size > 1 ? 's' : nil} '#{missing_tags * ','}' not found in include #{target_type}: #{include_file}" end advance # FIXME not accounting for skipped lines in reader line numbering push_include selected, include_file, path, inc_line_offset, attributes end else begin advance push_include open(include_file, 'r') {|f| f.read }, include_file, path, 1, attributes rescue warn %(asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}) replace_line %(Unresolved directive in #{@path} - include::#{target}[#{raw_attributes}]) return true end end true else false end end
def process_line line
def process_line line return line unless @process_lines if line.empty? @look_ahead += 1 return '' end # NOTE highly optimized if line.end_with?(']') && !line.start_with?('[') && line.include?('::') if line.include?('if') && (match = ConditionalDirectiveRx.match(line)) # if escaped, mark as processed and return line unescaped if line.start_with?('\\') @unescape_next_line = true @look_ahead += 1 line[1..-1] else if preprocess_conditional_inclusion(*match.captures) # move the pointer past the conditional line advance # treat next line as uncharted territory nil else # the line was not a valid conditional line # mark it as visited and return it @look_ahead += 1 line end end elsif @skipping advance nil elsif ((escaped = line.start_with?('\\include::')) || line.start_with?('include::')) && (match = IncludeDirectiveRx.match(line)) # if escaped, mark as processed and return line unescaped if escaped @unescape_next_line = true @look_ahead += 1 line[1..-1] else # QUESTION should we strip whitespace from raw attributes in Substitutors#parse_attributes? (check perf) if preprocess_include match[1], match[2].strip # peek again since the content has changed nil else # the line was not a valid include line and is unchanged # mark it as visited and return it @look_ahead += 1 line end end else # NOTE optimization to inline super @look_ahead += 1 line end elsif @skipping advance nil else # NOTE optimization to inline super @look_ahead += 1 line end end
def push_include data, file = nil, path = nil, lineno = 1, attributes = {}
reader.push_include data, file, path
data = IO.read file
file = File.expand_path path
path = 'partial.adoc'
Examples
read from the target specified.
This method is typically used in an IncludeProcessor to add source
based on the file, document-relative path and line information given.
Public: Push source onto the front of the reader and switch the context
def push_include data, file = nil, path = nil, lineno = 1, attributes = {} @include_stack << [@lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines] if file @file = file @dir = File.dirname file # only process lines in AsciiDoc files @process_lines = ASCIIDOC_EXTENSIONS[::File.extname(file)] else @file = nil @dir = '.' # right? # we don't know what file type we have, so assume AsciiDoc @process_lines = true end @path = if path @includes << Helpers.rootname(path) path else '<stdin>' end @lineno = lineno if attributes.has_key? 'depth' depth = attributes['depth'].to_i depth = 1 if depth <= 0 @maxdepth = {:abs => (@include_stack.size - 1) + depth, :rel => depth} end # effectively fill the buffer if (@lines = prepare_lines data, :normalize => true, :condense => false, :indent => attributes['indent']).empty? pop_include else # FIXME we eventually want to handle leveloffset without affecting the lines if attributes.has_key? 'leveloffset' @lines.unshift '' @lines.unshift %(:leveloffset: #{attributes['leveloffset']}) @lines.push '' if (old_leveloffset = @document.attr 'leveloffset') @lines.push %(:leveloffset: #{old_leveloffset}) else @lines.push ':leveloffset!:' end # compensate for these extra lines @lineno -= 2 end # FIXME kind of a hack #Document::AttributeEntry.new('infile', @file).save_to_next_block @document #Document::AttributeEntry.new('indir', @dir).save_to_next_block @document @eof = false @look_ahead = 0 end nil end
def resolve_expr_val(str)
# => "value"
resolve_expr_val(expr)
expr = '"{name}"'
@document.attributes['name'] = 'value'
# => 2
resolve_expr_val(expr)
expr = '2'
# => nil
resolve_expr_val(expr)
expr = '{undefined}'
# => ""
resolve_expr_val(expr)
expr = '"{undefined}"'
# => "\"value"
resolve_expr_val(expr)
expr = '"value'
# => "value"
resolve_expr_val(expr)
expr = '"value"'
Examples
Private: Resolve the value of one side of the expression
def resolve_expr_val(str) val = str type = nil if val.start_with?('"') && val.end_with?('"') || val.start_with?('\'') && val.end_with?('\'') type = :string val = val[1...-1] end # QUESTION should we substitute first? if val.include? '{' val = @document.sub_attributes val end unless type == :string if val.empty? val = nil elsif val.strip.empty? val = ' ' elsif val == 'true' val = true elsif val == 'false' val = false elsif val.include?('.') val = val.to_f else # fallback to coercing to integer, since we # require string values to be explicitly quoted val = val.to_i end end val end
def shift
also, we now have the field in the super class, so perhaps
TODO Document this override
def shift if @unescape_next_line @unescape_next_line = false super[1..-1] else super end end
def skip_front_matter! data, increment_linenos = true
def skip_front_matter! data, increment_linenos = true front_matter = nil if data[0] == '---' original_data = data.dup front_matter = [] data.shift @lineno += 1 if increment_linenos while !data.empty? && data[0] != '---' front_matter.push data.shift @lineno += 1 if increment_linenos end if data.empty? data.unshift(*original_data) @lineno = 0 if increment_linenos front_matter = nil else data.shift @lineno += 1 if increment_linenos end end front_matter end
def to_s
def to_s %(#<#{self.class}@#{object_id} {path: #{@path.inspect}, line #: #{@lineno}, include depth: #{@include_stack.size}, include stack: [#{@include_stack.map {|inc| inc.to_s}.join ', '}]}>) end