require 'fileutils' Plugins = File.expand_path(File.join(File.dirname(__FILE__), "../dsl")) TTY = ::File.open("/dev/tty", "w") require_relative "#{Plugins}/pyggish" class Enumerator def remaining array = [] loop { array << self.next } array end end class Livetext VERSION = "0.6.0" Space = " " Disallowed = [:_data=, :nil?, :===, :=~, :!~, :eql?, :hash, :<=>, :class, :singleton_class, :clone, :dup, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :freeze, :frozen?, :to_s, :inspect, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variables, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :send, :public_send, :respond_to?, :extend, :display, :method, :public_method, :singleton_method, :define_singleton_method, :object_id, :to_enum, :enum_for, :pretty_inspect, :==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__, :__binding__] class << self attr_reader :main end def self.handle_line(line) nomarkup = true sigil = "." scomment = rx(sigil, Livetext::Space) # apply these in order sname = rx(sigil) case when line =~ scomment handle_scomment(sigil, line) when line =~ sname handle_sname(sigil, line) else obj = @main # Livetext::Objects[sigil] obj._passthru(line) end end def self.handle_file(file) fname = "<<none>>" if file.is_a? String fname = file file = File.new(fname) end source = file.each_line @main = Livetext::System.new(source) @main._pushfile(fname) @main.file = fname @main.lnum = 0 loop do line = @main._next_line handle_line(line) end val = @main.finalize if @main.respond_to?(:finalize) val rescue => err STDERR.puts "handle_file: #{err}" end def self.rx(str, space=nil) Regexp.compile("^" + Regexp.escape(str) + "#{space}") end def self.handle_scomment(sigil, line) end def self._disallowed?(name) Livetext::Disallowed.include?(name.to_sym) end def self._get_name(obj, sigil, line) blank = line.index(" ") || line.index("\n") name = line[1..(blank-1)] abort "#{obj.where}: Name '#{name}' is not permitted" if _disallowed?(name) obj._data = line[(blank+1)..-1] name = "_def" if name == "def" name = "_include" if name == "include" abort "#{obj.where}: mismatched 'end'" if name == "end" name end def self.handle_sname(sigil, line) obj = @main # Livetext::Objects[sigil] name = _get_name(obj, sigil, line) # unless obj.respond_to?(name) # abort "#{obj.where}: '#{name}' is unknown" # return # end if name == "notes" # FIXME wtf obj.notes else obj.send(name) end rescue => err STDERR.puts "ERROR on #{obj.file} line #{obj.lnum}" STDERR.puts err.backtrace end end class Livetext::Functions # Functions will go here... user-def AND pre-def?? def date Time.now.strftime("%F") end def time Time.now.strftime("%F") end def basename file = ::Livetext.main.file ::File.basename(file, ".*") end end module Livetext::Helpers def _check_existence(file) raise "No such file found" unless File.exist?(file) end def _source @input end def _data=(str) str ||= "" @_data = str @_args = str.split end def _data @_data end def _args if block_given? @_args.each {|arg| yield arg } else @_args end end def _optional_blank_line @line = _next_line if _peek_next_line =~ /^ *$/ end def _comment?(str, sigil=".") c1 = sigil + Livetext::Space c2 = sigil + sigil + Livetext::Space str.index(c1) == 0 || str.index(c2) == 0 end def _trailing?(char) return true if ["\n", " ", nil].include?(char) return false end def _end?(str, sigil=".") cmd = sigil + "end" return false if str.index(cmd) != 0 return false unless _trailing?(str[5]) return true end def _raw_body(tag = "__EOF__", sigil = ".") lines = [] loop do @line = _next_line break if @line.chomp.strip == tag lines << @line end _optional_blank_line if block_given? lines.each {|line| yield @line } else lines end end def _body(sigil=".") lines = [] loop do @line = _next_line # no chomp needed break if _end?(@line, sigil) next if _comment?(@line, sigil) lines << @line # _formatting(line) # FIXME ?? end _optional_blank_line if block_given? lines.each {|line| yield line } else lines end end def _body!(sigil=".") _body(sigil).join("\n") end def _basic_format(line, delim, tag) s = line.each_char c = s.next last = nil getch = -> { last = c; c = s.next } buffer = "" loop do case c when " " buffer << " " last = " " when delim if last == " " || last == nil buffer << "<#{tag}>" c = getch.call if c == "(" loop { getch.call; break if c == ")"; buffer << c } buffer << "</#{tag}>" else loop { buffer << c; getch.call; break if c == " " || c == nil || c == "\n" } buffer << "</#{tag}>" buffer << " " if c == " " end else buffer << delim end else buffer << c end getch.call end buffer end def _handle_escapes(str, set) str = str.dup set.each_char do |ch| str.gsub!("\\#{ch}", ch) end str end def _formatting(line) line = _basic_format(line, "_", "i") line = _basic_format(line, "*", "b") line = _basic_format(line, "`", "tt") line = _handle_escapes(line, "_*`") line end def OLD_formatting(line) l2 = _formatting(line) line.replace(l2) return line end def _var_substitution(line) # FIXME handle functions separately later?? fobj = ::Livetext::Functions.new @funcs = ::Livetext::Functions.instance_methods @funcs.each do |func| name = ::Regexp.escape("$$#{func}") rx = /#{name}\b/ line.gsub!(rx) do |str| val = fobj.send(func) str.sub(rx, val) end end @vars.each_pair do |var, val| name = ::Regexp.escape("$#{var}") rx = /#{name}\b/ line.gsub!(rx, val) end line end def _passthru(line) return if @_nopass _puts "<p>" if line == "\n" and ! @_nopara OLD_formatting(line) _var_substitution(line) _puts line end def _puts(*args) @output.puts *args end def _print(*args) @output.print *args end def _peek_next_line @input.peek end def _next_line @line = @input.next @lnum += 1 _debug "Line: #@lnum: #@line" @line end def _debug=(val) @_debug = val end def _debug(*args) TTY.puts *args if @_debug end end module Livetext::Standard def comment junk = _body # do nothing with contents end def shell cmd = _data _errout("Running: #{cmd}") system(cmd) end def func fname = @_args[0] # FIXME: don't permit 'initialize' (others?) func_def = <<-EOS def #{fname} #{_body!} end EOS ::Livetext::Functions.class_eval func_def end def shell! cmd = _data system(cmd) end def errout TTY.puts _data end def say str = _var_substitution(_data) _optional_blank_line end def banner str = _var_substitution(_data) n = str.length - 1 _errout "-"*n _errout str _errout "-"*n end def quit @output.close exit end def outdir @_outdir = @_args.first _optional_blank_line end def outdir! # FIXME ? @_outdir = @_args.first raise "No output directory specified" if @_outdir.nil? raise "No output directory specified" if @_outdir.empty? system("rm -f #@_outdir/*.html") _optional_blank_line end def _output(name) @output.close unless @output == STDOUT @output = File.open(@_outdir + "/" + name, "w") @output.puts "<meta charset='UTF-8'>\n\n" end def _append(name) @output.close unless @output == STDOUT @output = File.open(@_outdir + "/" + name, "a") @output.puts "<meta charset='UTF-8'>\n\n" end def output name = @_args.first _debug "Redirecting output to: #{name}" _output(name) end def append file = @_args[0] _append(file) end def next_output tag, num = @_args _next_output(tag, num) _optional_blank_line end def cleanup @_args.each do |item| if ::File.directory?(item) system("rm -f #{item}/*") else ::FileUtils.rm(item) end end end def _next_output(tag = "sec", num = nil) @_file_num = num ? num : @_file_num + 1 @_file_num = @_file_num.to_i name = "#{'%03d' % @_file_num}-#{tag}.html" _output(name) end def _def name = _args[0] str = "def #{name}\n" str += _body! str += "end\n" eval str rescue => err STDERR.puts "Syntax error in definition:\n#{err}\n#$!" end def nopass @_nopass = true end def set assigns = _data.chomp.split(/, */) assigns.each do |a| var, val = a.split("=") val = val[1..-2] if val[0] == ?" and val[-1] == ?" val = val[1..-2] if val[0] == ?' and val[-1] == ?' @vars[var] = val end _optional_blank_line end def _pushfile(fname) @source_files ||= [] @source_files.push(@file) @file = fname @file end def _popfile @file = @source_files.pop end def _include file = _args.first lines = ::File.readlines(file) _pushfile(file) # STDERR.puts "_include: ****** Set @file = #@file" lines.each {|line| _debug " inc: #{line}" } rem = @input.remaining array = lines + rem @input = array.each # FIXME .with_index _optional_blank_line _popfile end def include! file = _args.first _pushfile existing = File.exist?(file) return if not existing lines = ::File.readlines(file) File.delete(file) lines.each {|line| _debug " inc: #{line}" } rem = @input.remaining array = lines + rem @input = array.each # FIXME .with_index _optional_blank_line _popfile end def mixin name = _args.first # Expect a module name file = "#{Plugins}/" + name.downcase + ".rb" return if @_mixins.include?(file) file = "./#{name}.rb" unless File.exist?(file) _check_existence(file) @_mixins << file _pushfile(file) newmod = Module.new $mods << newmod Object.const_set(name.capitalize, newmod) newmod.instance_eval(File.read(file)) init = "init_#{name}" self.send(init) if self.respond_to? init _optional_blank_line _popfile end def old_mixin name = _args.first file = "#{Plugins}/" + name + ".rb" return if @_mixins.include?(file) file = "./#{name}.rb" unless File.exist?(file) raise "No such file: #{name}.rb found" unless File.exist?(file) @_mixins << file _pushfile(file) main = Livetext.main m0 = main.methods.reject {|x| x.to_s[0] == "_" } self.class.class_eval(::File.read(file)) m1 = main.methods.reject {|x| x.to_s[0] == "_" } $meths[file] = m1 - m0 init = "init_#{name}" self.send(init) if self.respond_to? init _optional_blank_line _popfile end def copy file = _args.first _pushfile(file) text = ::File.readlines(file) @output.puts text _optional_blank_line _popfile end def r _puts _data # No processing at all end def raw # No processing at all (terminate with __EOF__) _puts _raw_body end def debug arg = _args.first self._debug = true self._debug = false if arg == "off" end def nopara @_nopara = true end def heading _print "<center><font size=+1><b>" _print _data _print "</b></font></center>" end def newpage _puts '<p style="page-break-after:always;"></p>' _puts "<p/>" end def invoke(str) end def dlist delim = "~~" _puts "<table>" _body do |line| # TTY.puts "Line = #{line}" line = _formatting(line) # TTY.puts "Line = #{line}\n " term, defn = line.split(delim) _puts "<tr>" _puts "<td width=3%><td width=10%>#{term}</td><td>#{defn}</td>" _puts "</tr>" end _puts "</table>" end end class Livetext METHS = (Livetext::Standard.instance_methods - Object.methods).sort end class Livetext::System < BasicObject include ::Kernel include ::Livetext::Helpers include ::Livetext::Standard attr_accessor :file, :lnum def initialize(input = ::STDIN, output = ::STDOUT) @input = input @output = output @vars = {} @_mixins = [] @_outdir = "." @_file_num = 0 @_nopass = false @_nopara = false @lnum = 0 end def where "Line #@lnum of #@file" end def method_missing(name, *args) # TTY.puts $mods.inspect $mods.reverse.each do |mod| #TTY.puts "mod methods = #{mod.module_methods.inspect}" if mod.respond_to?(name) mod.send(name, *args) return end end TTY.puts "Got here" _puts " Error: Method '#{name}' is not defined." puts caller.map {|x| " " + x } exit end end $mods = [] if $0 == __FILE__ Livetext.handle_file(ARGV[0] || STDIN) end