#!/usr/bin/env ruby # encoding: binary (rage) # This file is part of Metasm, the Ruby assembly manipulation suite # Copyright (C) 2006-2009 Yoann GUILLOT # # Licence is LGPL, see LICENCE in the top-level directory # This scripts is used to compile the Metasm documentation into html files # Losely inspired from the rst syntax # stuff to generate html code module Html class Elem attr_reader :name, :attrs, :content, :style IndentAdd = ' ' LineLenMax = 80 def initialize(name, attrs=nil, content=nil) @name = name @attrs = Hash.new @style = Hash.new attrs.each { |k, v| set_attr(k, v) } if attrs if content == false @content = Array.new @uniq = true else @content = content ? content : Array.new @uniq = false end self end @@quotechars = { 'è' => 'è', 'ë' => 'ë', '>' => '>', '<' => '<', '"' => '"', '&' => '&', } def add(*content) content.each { |e| if (e.class == Array) add(*e) next end if e.class.ancestors.include? Elem @content << e else @content << e.to_s.gsub(Regexp.new("(#{@@quotechars.keys.join('|')})", 'm')) { |x| @@quotechars[x] } end } self end alias << add def add_style(k, v) @style[k] = v self end def set_attr(k, v) if k == 'style' v.split(/\s*;\s*/).each { |s| add_style($1, $2) if s =~ /^\s*(\S+)\s*:\s*(.*?)\s*$/ } else @attrs[k]=v end self end def bg(c) @style['background'] = c self end def hclass(c) @attrs['class'] = c self end def length(start=nil) # text length on one line w/o indent if start l = start.length else # '' l = @name.length + 2 @attrs.each{ |k, v| l += " #{k}=\"#{v}\"".length } # ' style=""' - last '; ' l += 9-2 unless @style.empty? # 'k: v; ' @style.each{ |k, v| l += "#{k}: #{v}; ".length } # ' /' l += 2 if @uniq end @content.each{ |c| l += c.length } # '' l += 3+@name.length unless @uniq return l end def to_s(indent = '') attrs = @attrs.map { |k, v| " #{k}=\"#{v}\"" }.join attrs += ' style="' + @style.map{ |k, v| "#{k}: #{v}" }.join('; ') + '"' unless @style.empty? s = '' << indent << '<' << @name << attrs << (@uniq ? ' />' : '>') if @uniq s elsif @name == 'pre' s << @content.map { |c| c.to_s }.join.chomp << '' else if length(s) > LineLenMax sindent = indent + IndentAdd sep = "\n" @content.each { |c| case c when Elem if sep == '' s << c.to_s(sindent).sub(/^\s+/, '') else news = c.to_s(sindent) plen = s.length - (s.rindex("\n") || -1) - 1 plen -= 1 if s[-1, 1] == ' ' newss = news.sub(/^\s+/, '') if not news.include?("\n") and s[-1] != ?> and plen + 1 + newss.length <= LineLenMax # concat inline tag to previous String s << ' ' if s[-1, 1] != ' ' s << newss else s << sep if c.name =~ /^h\d$/ and c != @content.first s << sep << news end end when String cw = c.split(/\s+/) if @name == 'p' and c.object_id == @content.first.object_id cw.shift if cw[0] == '' s << "\n" << sindent else s << cw.shift.to_s end plen = s.length - (s.rindex("\n") || -1) - 1 while w = cw.shift plen -= 1 if s[-1, 1] == ' ' if plen + 1 + w.length > LineLenMax s << "\n" << sindent plen = sindent.length end s << ' ' if s[-1, 1] != ' ' s << w plen += w.length+1 end if c !~ /\s+$/ sep = '' next end else s << sep << sindent << c.to_s end sep = "\n" } sep = "\n" if @name == 'p' sep << indent if sep != '' s << sep << "" else s << @content.map { |c| c.to_s }.join << "" end end end def inspect "<#{@name}"+@content.map{|c|"\n"+c.inspect}.join+"\n/#{@name}>" end end class Page < Elem attr_reader :body, :head def initialize @body = Elem.new('body') @head = Elem.new('head') super('html', {'xmlns'=>'http://www.w3.org/1999/xhtml', 'xml:lang'=>'fr'}) add(@head) add(@body) end def to_s ''+"\n"+ ''+"\n"+ super.to_s end end class Img < Elem def initialize(src, alt=nil) super('img', {'src'=>src}, false) set_attr('alt', alt) if alt self end end class A < Elem def initialize(href, text) super('a', {'href'=>href}, [text]) end end class P < Elem def initialize(content = nil) super('p') add(content) if content self end end class Div < Elem def initialize(hclass = nil) super('div') hclass(hclass) if hclass self end end class Span < Elem def initialize(hclass = nil) super('span') hclass(hclass) if hclass self end end class Stylesheet < Elem def initialize(href) super('link', {'rel'=>'stylesheet', 'type'=>'text/css', 'href'=>href}, false) end end class Br < Elem def initialize super('br', nil, false) end end class Hr < Elem def initialize super('hr', nil, false) end end class List < Elem def initialize(*elems) super('ul') elems.each { |e| add_line(e) } end def add_line(line) add(Elem.new('li').add(line)) self end end end class Txt2Html def initialize(f) @@done ||= [] return if @@done.include? f @@done << f raise 'bad path' if (f.split('/') & ['.', '..']).first outf = outfilename(f) puts "compiling #{outf}..." if $VERBOSE @pathfix = outf.split('/')[0...-1].map { '../' }.join out = compile(File.open(f, 'rb') { |fd| fd.read }.gsub("\r", '') + "\n\n") File.open(outf, 'wb') { |fd| fd.write out.to_s.gsub("\r", '').gsub("\n", "\r\n") } end def outfilename(f) f.sub(/\.txt$/, '') + '.html' end def compile(raw) prev = '' state = {} anchors = {} out = Html::Page.new out.head << Html::Stylesheet.new(@pathfix + 'style.css') flush = lambda { out.body << Html::P.new(compile_string(prev)) if prev.length > 0 [:pre, :list, :par].each { |f| state.delete f } prev = '' } raw.each_line { |l| case l = l.chomp when /^([=#*-])\1{3,}$/ if prev.length > 0 # title if not state[:h1] or state[:h1] == $1 state[:h1] = $1 e = 'h1' elsif not state[:h2] or state[:h2] == $1 state[:h2] = $1 e = 'h2' elsif not state[:h3] or state[:h3] == $1 state[:h3] = $1 e = 'h3' else raise "unknown title level after #{prev.inspect}" end str = compile_string(prev) state[:title] ||= str if e == 'h1' if id = prev[/[a-z]\w+/i] id = id.downcase id += '_' while anchors[id] anchors[id] = true attr = { 'id' => id } end out.body << Html::Elem.new(e, attr).add(str) prev = '' flush[] else # horizontal rule out.body << Html::Hr.new flush[] end when /^([*-]+)\s+(.*)/ # list bullet = $1 text = $2 if lst = state[:list] && state[:list][bullet] state[:list].delete_if { |k, v| k.length > bullet.length } else flush[] if not state[:list] state[:list] ||= {} state[:list].delete_if { |k, v| k.length > bullet.length } lst = state[:list][bullet] = Html::List.new if pl = state[:list][bullet.chop] pl.content.last.content << lst else out.body << lst end end lst.add_line compile_string(text) when /^\s+(\S.*)$/ # preformatted text if not pre = state[:pre] flush[] pre = state[:pre] = Html::Elem.new('pre') out.body << pre end pre.add compile_string(l) + ["\n"] when /^\s*$/ flush[] else if state[:list] lst = state[:list].sort.last[1] lst.content.last.content << ' ' << compile_string(l) else prev << ' ' if prev.length > 0 prev << l end end } flush[] out.head << Html::Elem.new('title').add(state[:title]) if state[:title] out end # handle **bold_words** *italic* `fixed` **bold__word__with__underscore** def compile_string(str) o = [str] on = [] o.each { |s| while s.kind_of? String and o1 = s.index('**') and o2 = s.index('**', o1+2) and not s[o1..o2].index(' ') on << s[0...o1] << Html::Elem.new('b').add(s[o1+2...o2].tr('_', ' ').gsub(' ', '_')) s = s[o2+2..-1] end on << s } o = on on = [] o.each { |s| while s.kind_of? String and o1 = s.index('*') and o2 = s.index('*', o1+1) and not s[o1..o2].index(' ') on << s[0...o1] << Html::Elem.new('i').add(s[o1+1...o2].tr('_', ' ').gsub(' ', '_')) s = s[o2+1..-1] end on << s } o = on on = [] o.each { |s| while s.kind_of? String and o1 = s.index('`') and o2 = s.index('`', o1+1) on << s[0...o1] << Html::Span.new('quote').add(s[o1+1...o2]) s = s[o2+1..-1] end on << s } o = on on = [] o.each { |s| while s.kind_of? String and o1 = s.index('<') and o2 = s.index('>', o1+1) and not s[o1..o2].index(' ') on << s[0...o1] lnk = s[o1+1...o2] s = s[o2+1..-1] if File.exist? lnk case lnk[/\.(\w+)$/, 1] when 'txt' tg = outfilename(lnk) Txt2Html.new(lnk) on << Html::A.new(@pathfix + tg, File.basename(lnk, '.txt').tr('_', ' ').gsub(' ', '_')) when 'jpg', 'png' on << Html::Img.new(lnk) end else on << Html::A.new(lnk, lnk) if lnk =~ /\.txt$/ @@seen_nofile ||= [] if not @@seen_nofile.include? lnk @@seen_nofile << lnk puts "reference to missing #{lnk.inspect}" end on.last.hclass('brokenlink') end end end on << s } o = on end end if __FILE__ == $0 $VERBOSE = true if ARGV.delete '-v' if ARGV.empty? Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '../doc'))) ARGV.concat Dir['**/index.txt'] end ARGV.each { |a| Txt2Html.new(a) } end