# encoding: utf-8 module Glyph # A Macro object is instantiated by a Glyph::Interpreter whenever a macro is found in the parsed text. # The Macro class contains shortcut methods to access the current node and document, as well as other # useful methods to be used in macro definitions. class Macro include Validators include Utils attr_reader :node, :source_name, :source_file, :source_topic, :name # Creates a new macro instance from a Node # @param [Node] node a node populated with macro data def initialize(node) @data = {} @node = node @name = @node[:name] @updated_source = nil @source_name = @node[:source][:name] || nil rescue "--" @source_topic = @node[:source][:topic] || nil rescue "--" @source_file = @node[:source][:file] rescue nil end # Resets the name of the updated source (call before calling # Macro#interpret) # @param [String] name the source name # @param [String] file the source file # @param [String] topic the topic file # @since 0.3.0 def update_source(name, file=nil, topic=nil) file ||= @node[:source][:file] rescue nil @updated_source = {:name => name, :file => file, :topic => topic} end # Returns a Glyph code representation of the specified parameter # @param [Fixnum] n the index of the parameter # @return [String, nil] the string representation of the parameter # @since 0.3.0 def raw_parameter(n) @node.parameter(n).contents.to_s rescue nil end # Returns a Glyph code representation of the specified attribute # @param [String, Symbol] name the name of the attribute # @return [String, nil] the string representation of the attribute # @since 0.3.0 def raw_attribute(name) @node.attribute(name).contents.to_s rescue nil end # Returns an evaluated macro attribute by name # @param [String, Symbol] name the name of the attribute # @param [Hash] options a hash of options # @option options [Boolean] :strip whether the value is stripped or not # @return [String, nil] the value of the attribute # @since 0.3.0 def attribute(name, options={:strip => true, :null_if_blank => true}) return @attributes[name.to_sym] if @attributes && @attributes[name.to_sym] return nil unless @node.attribute(name) @attributes = {} unless @attributes @attributes[name] = @node.attribute(name).evaluate(@node, :attrs => true).to_s @attributes[name].strip! if options[:strip] @attributes[name] = nil if @attributes[name].blank? && options[:null_if_blank] @attributes[name] end # Returns an evaluated macro parameter by index # @param [Fixnum] n the index of the parameter # @param [Hash] options a hash of options # @option options [Boolean] :strip whether the value is stripped or not # @return [String, nil] the value of the parameter # @since 0.3.0 def parameter(n, options={:strip => true, :null_if_blank => true}) return @parameters[n] if @parameters && @parameters[n] return nil unless @node.parameter(n) @parameters = Array.new(@node.parameters.length) unless @parameters @parameters[n] = @node.parameter(n).evaluate(@node, :params => true).to_s @parameters[n].strip! if options[:strip] @parameters[n] = nil if @parameters[n].blank? && options[:null_if_blank] @parameters[n] end # Returns a hash containing all evaluated macro attributes # @param [Hash] options a hash of options # @option options [Boolean] :strip whether the value is stripped or not # @return [Hash] the macro attributes # @since 0.3.0 def attributes(options={:strip => true, :null_if_blank => true}) return @attributes if @attributes @attributes = {} @node.attributes.each do |value| @attributes[value[:name]] = value.evaluate(@node, :attrs => true) @attributes[value[:name]].strip! if options[:strip] @attributes[value[:name]] = nil if @attributes[value[:name]].blank? && options[:null_if_blank] end @attributes end # Returns an array containing all evaluated macro parameters # @param [Hash] options a hash of options # @option options [Boolean] :strip whether the value is stripped or not # @return [Array] the macro parameters # @since 0.3.0 def parameters(options={:strip => true, :null_if_blank => true}) return @parameters if @parameters @parameters = [] @node.parameters.each do |value| @parameters << value.evaluate(@node, :params => true) @parameters[@parameters.length-1].strip! if options[:strip] @parameters[@parameters.length-1] = nil if @parameters.last.blank? && options[:null_if_blank] end @parameters end alias params parameters alias param parameter alias attrs attributes alias attr attribute alias raw_param raw_parameter alias raw_attr raw_attribute # Equivalent to Glyph::Macro#parameter(0). # @since 0.3.0 def value parameter(0) end # Equivalent to Glyph::Macro#raw_parameter(0). # @since 0.3.0 def raw_value raw_parameter(0) end # Returns the "path" to the macro within the syntax tree. # @return [String] the macro path # @since 0.3.0 def path macros = [] @node.ascend do |n| case when n.is_a?(Glyph::MacroNode) then if n[:name] == :"|xml|" then name = "xml[#{n[:element]}]" else break if n[:name] == :include name = n[:name].to_s end when n.is_a?(Glyph::ParameterNode) then if n.parent.parameters.length == 1 then name = nil else name = n[:name].to_s end when n.is_a?(Glyph::AttributeNode) then name = "@#{n[:name]}" else name = nil end macros << name end macros.reverse.compact.join('/') end # Returns a todo message to include in the document in case of errors. # @param [String] message the message to include in the document # @return [String] the resulting todo message # @since 0.2.0 def macro_todo(message) draft = Glyph['document.draft'] Glyph['document.draft'] = true unless draft res = interpret "![#{message}]" Glyph['document.draft'] = false unless draft res end # Raises a macro error (preventing document post-processing) # @param [String] msg the message to print # @raise [Glyph::MacroError] def macro_error(msg, klass=Glyph::MacroError) @node[:document].errors << msg if @node[:document] raise klass.new(msg, self) end # Prints a macro earning # @param [String] msg the message to print # @param [Exception] e the exception raised # @since 0.2.0 def macro_warning(msg, e=nil) if e.is_a?(Glyph::MacroError) then e.display else message = "#{msg}\n source: #{@source_name}\n path: #{path}" if Glyph.debug? then message << %{\n#{"-"*54}\n#{@node.to_s.gsub(/\t/, ' ')}\n#{"-"*54}} if e then message << "\n"+"-"*20+"[ Backtrace: ]"+"-"*20 message << "\n"+e.backtrace.join("\n") message << "\n"+"-"*54 end end Glyph.warning message end end # Instantiates a Glyph::Interpreter and interprets a string # @param [String] string the string to interpret # @return [String] the interpreted output def interpret(string) @node[:escape] ? string : inject(string).document.output end # @see Glyph::Document#placeholder def placeholder(&block) @node[:document].placeholder &block end # @see Glyph::Document#bookmark def bookmark(hash) @node[:document].bookmark hash end # @see Glyph::Document#bookmark? def bookmark?(ident) @node[:document].bookmark? ident end # @see Glyph::Document#header? def header?(ident) @node[:document].header? ident end # @see Glyph::Document#header def header(hash) @node[:document].header hash end # @see Glyph::Document#snippet def snippet(key, value) @node[:document].snippet key, value end # @see Glyph::Document#snippet? def snippet?(ident) @node[:document].snippet? ident end # @since 0.5.0 # Renders a macro representation # @param [Symbol, String] rep the representation to render # @param [Hash] data the data to pass to the representation def render(rep=nil, data=nil) rep ||= @name data ||= @data block = Glyph::REPS[rep.to_sym] macro_error "No macro representation for '#{rep}'", e unless block instance_exec(data, &block).to_s end # Stores a block of code to be "dispatched" via macro composition # @since 0.5.0 # @example # # Macro definition (Ruby) # macro :greet do # dispatch do |node| # "#{node[:name]}, #{node.param 0}!" # end # end # # # macro usage (Glyph) # greet/Hello[World] --[Outputs: Hello, World!] # greet/GoodBye[John] --[Outputs: Goodbye, John!] def dispatch(&block) @node[:dispatch] = block value end # Performs parameter/attribute substitution and interprets text # @since 0.5.0 # @param [String] text the text to interpret # @return [String] the interpreted output def apply(text) body = text.dup # Parameters body.gsub!(/\{\{(\d+)\}\}/) do raw_param($1.to_i).to_s.strip end # Attributes body.gsub!(/\{\{([^\[\]\|\\\s]+)\}\}/) do raw_attr($1.to_sym).to_s.strip end interpret body end # Parses text and injects the syntax tree into the current node # @since 0.5.0 # @param [String] text the text to parse # @return [Glyph::Interpreter] the interpreter instance used to parse text def inject(text) context = create_context interpreter = Glyph::Interpreter.new text, context subtree = interpreter.parse subtree[:source] = context[:source] @node << subtree interpreter end # Parses text # @since 0.5.0 # @param [String] text the text to parse # @return [Glyph::Node] the syntax tree generated by parsing def parse(text) Glyph::Interpreter.new(text, create_context).parse end # Executes a macro definition in the context of self def expand block = Glyph::MACROS[@name] macro_error "Undefined macro '#@name'" unless block instance_exec(@node, &block).to_s.gsub(/\\?([\[\]\|])/){"\\#$1"} end private def create_context context = {} context[:source] = @updated_source || @node[:source] context[:embedded] = true context[:document] = @node[:document] context end end end