#!/usr/bin/ruby -w

# generates core.css, which defines the 'display' property for elements where
# it can guess the correct value

# To use this script, you need to have a copy of the source of 'Docbook, the
# Definative Guide', which you can obtain though CVS like this,
#
#   export CVS_RSH=ssh
#   cvs -z3 -d:pserver:anonymous@cvs.docbook.sourceforge.net:/cvsroot/docbook co defguide
#
# Put this script in the same place as the 'defguide' directory you just
# checked out and run it,
#
#   ./makecss.rb
#
#
# If you make a file called 'extra-block-elements.txt' the script will read
# one element name per line from it, and include these in the list of elements
# marked { display:block; }
#
#							-- David Holroyd


# apt-get install librexml-ruby
require "rexml/document"

require 'date'

$refpath = "defguide/en/refpages/elements"

$block_elements = Array.new
$inline_elements = Array.new
$suppressed_elements = Array.new
$context_dependant_elements = Array.new
$linespecific_formatted_elements = Array.new
$undefined_elements = Array.new

$all_elements = Hash.new


class ListenerDecorator
  attr_accessor :parent, :listener

  def bind() end

  def unbind() end

  def tag_start(name, attrs) nil end

  def tag_end(name) false end

  def text(text) false end

  def instruction(target, attrs) false end

  def comment(text) false end

  def cdata(text) false end
end

class ExpectationDecorator < ListenerDecorator
  def bind()
    @expectation_text = ""
  end

  def unbind()
    @listener.formatting_expectation += @expectation_text
  end

  def tag_start(name, attrs)
    case name
      when "quote"
        @expectation_text += "'"
      when "sgmltag"
        @expectation_text += "<"
    end
    nil
  end

  def tag_end(name)
    case name
      when "quote"
        @expectation_text += "'"
      when "sgmltag"
        @expectation_text += ">"
    end
    false
  end

  def text(text)
    @expectation_text += text
    true
  end
end

class TitleDecorator < ListenerDecorator
  def text(text)
    if text =~ /Processing expectations/
      parent.processing_expectation = true
    end
    true
  end
end


class Refsect2Decorator < ListenerDecorator
  def bind
    @processing_expectation=false
  end

  attr_accessor :processing_expectation

  def tag_start(name, attrs)
    if name=="title"
      TitleDecorator.new
    elsif name=="para" && @processing_expectation
      ExpectationDecorator.new
    else
      nil
    end
  end
end

class BaseDecorator < ListenerDecorator
  def tag_start(name, attrs)
    if name=="refsect2"
      Refsect2Decorator.new
    else
      nil
    end
  end
end


# This listens to parse events coming from ReXML and delegates them off to
# a 'decorator' object.  When a decorator sees a start tag, it may choose to
# push a new decorator object onto the listener's stack (it will pop off again
# when the corresponding close-tag is encountered)
# 
class MyListener
  def initialize
    @stack = Array.new
    @decorator_stack = Array.new
    @decorator_stack << BaseDecorator.new
    @formatting_expectation = ""
  end

  attr_accessor :formatting_expectation

  def tag_start(name, attrs)
    @stack << name
    decorator = get_decorator.tag_start(name, attrs)
    unless decorator.nil?
      decorator.parent = get_decorator
      decorator.listener = self
      decorator.bind()
    end
    @decorator_stack << decorator
  end

  def tag_end(name)
    decorator = @decorator_stack.pop
    decorator.unbind() unless decorator.nil?
    top = @stack.pop
    raise "found </#{name}>, expecting </#{top}>" unless top==name
    decorate() do |decorator|
      return if decorator.tag_end(name)
    end
  end

  def text(text)
    decorate() do |decorator|
      return if decorator.text(text)
    end
  end

  def instruction(target, attrs)
    decorate() do |decorator|
      return if decorator.instruction(target, attrs)
    end
  end

  def comment(text)
    decorate() do |decorator|
      return if decorator.comment(text)
    end
  end

  def cdata(text)
    decorate() do |decorator|
      return if decorator.cdata(text)
    end
  end

 protected
  def decorate()
    @decorator_stack.reverse_each do |decorator|
      yield decorator unless decorator.nil?
    end
  end

  def get_decorator
    @decorator_stack.reverse_each do |decorator|
      return decorator unless decorator.nil?
    end
    return nil
  end
end

def get_formatting_expectation(ref_file)
  listener = MyListener.new
  file = File.new(ref_file)
  doc = REXML::Document.parse_stream(file, listener)
  return listener.formatting_expectation
end


# Looks at the XML entities in the given text to determine how to format
# some element.  Also, ASCIfies some character-entities to make aux.css
# comments look purty
# 
def define_formatting(tag, expectations)
  options = Array.new
  if expectations.nil?
    expectations = ""
  else
    expectations.gsub!(/&([^;]+);/) do |match|
      case $1
        when "format.block"
	  options << "block"
	  ""
	when "format.inline"
	  options << "inline"
	  ""
	when "format.context"
	  options << "context"
	  ""
	when "format.suppress"
	  options << "suppress"
	  ""
	when "format.csuppress"
	  options << "csuppress"
	  "Sometimes suppressed."
	when "pexp.linespecific"
	  options << "linespecific"
	  ""
	when "calssemantics"
	  options << "calssemantics"
	  ""
	when "pexp.moreinfo"
	  "The MoreInfo attribute can help generate a link or query to retrieve additional information."
	when "UNIX"
	  "UN*X"
	when "DTD"
	  "DTD"
	when "HTML"
	  "HTML"
	when "XML"
	  "XML"
	when "SGML"
	  "SGML"
	when "CALS"
	  "CALS"
	when "ldquo", "rdquo"
	  "\""
	when "lsquo", "rsquo"
	  "'"
	when "laquo"
	  "<<"
	when "raquo"
	  ">>"
	when "nbsp"
	  " "
	when "copy"
	  "(c)"
	when "hellip"
	  "..."
	when "amp"
	  "&"
	when "lt"
	  "<"
	when "gt"
	  ">"
	else
	  raise "don't know about entity #{match} in processing expectations for <#{tag}>"
      end
    end
    expectations.sub!(/^\s+/, "")
    expectations.sub!(/\s+$/, "")
  end
  if options.include?("inline") && options.include?("block")
    raise "<#{tag}> has inline and block"
  elsif options.include?("inline")
    $inline_elements << tag
  elsif options.include?("block")
    $block_elements << tag
  elsif options.include?("suppress")
    $suppressed_elements << tag
#  elsif options == ["context"]
#    $context_dependant_elements << tag
  elsif options.include?("linespecific")
    $linespecific_formatted_elements << tag
  else
    $undefined_elements << tag
#    puts "<#{tag}> : "+options.join(", ")
  end
  $all_elements[tag] = expectations
end

unless FileTest.directory?($refpath)
  raise "no dir '#{$refpath}'"
end


Dir.foreach($refpath) do |dir|
  next if (dir =~ /^\./)
  ref = "#{$refpath}/#{dir}/refentry.xml"
  if FileTest.exists?(ref)
    format = get_formatting_expectation(ref)
    define_formatting(dir, format)
  else
    $stderr.puts("no refentry.xml in #{dir}");
  end
end

def make_rule_for_all(io, elements, rule)
  line = ""
  last = elements.pop

  elements.sort.each do |tag|
    if block_given?
      tag = yield tag
    end
    if (line.size + tag.size + 2) > 79
      io.puts line
      line = ""
    end
    line += tag + ", "
  end
  if block_given?
    last = yield last
  end
  if (line.size + last.size + 2) > 79
    io.puts line
    line = ""
  end
  io.puts line + last + " {"
  io.puts "	#{rule}"
  io.puts "}"
end

# load any supplementry info

if FileTest.exists?("extra-block-elements.txt")
  File.open("extra-block-elements.txt") do |io|
    io.each_line do |element|
      element.strip!
      if $block_elements.include?(element) || $inline_elements.include?(element) || $suppressed_elements.include?(element)
        raise "<#{element}> already has a display type"
      end
      $block_elements << element
      $undefined_elements.delete(element)
    end
  end
end


File.open("core.css", File::CREAT|File::WRONLY|File::TRUNC) do |io|
  io.puts("/* Generated #{Date.today} */")
  make_rule_for_all(io, $inline_elements, "display:inline;")
  io.puts
  make_rule_for_all(io, $block_elements, "display:block;")
  io.puts
  make_rule_for_all(io, $suppressed_elements, "display:none;")
  io.puts
  make_rule_for_all(io, $linespecific_formatted_elements, "white-space:pre;\n\tfont-family:monospace;\n\tdisplay:block;")
end


# not sure what this will be good for -- room for future expansion
#
File.open("aux.css", File::CREAT|File::WRONLY|File::TRUNC) do |io|
  $all_elements.each do |key, val|
    unless val==""
      io.puts "/*"
      io.puts val
      io.puts "*/"
    end
    io.puts "#{key} {"
    io.puts "}"
    io.puts
  end
end

unless $undefined_elements.empty?
  puts "These elements have no display-type defined:"
  print "  "
  len = 2
  $undefined_elements.each do |el|
    if len + (el.size + 2) >=79
      puts
      print "  "
      len=2
    end
    print el
    print ", "
    len += (el.size + 2)
  end
end
puts
