require 'pp' require 'htemplate' require 'pathname' require 'bluecloth' require 'digest/md5' module Enid VERSION = "0.3.1" Entry = Struct.new(:entry, :children, :order) Title = Struct.new(:text, :ref) Text = Struct.new(:text) Link = Struct.new(:href, :text) ReverseLink = Struct.new(:href, :text) Attachment = Struct.new(:file, :text) class Top def text; ""; end def id; nil; end def href; "/"; end def pretty; "top"; end def match?(term); false; end def to_html; ""; end end class Entry TEMPLATE = HTemplate.new(<<'EOF')
$ unless @skip_root
$:entry.to_html $ if !children.empty? && @too_deep && id ... $ end
$ end $ if !children.empty? && !@too_deep
    $ children.each { |child|
  1. $:{child.to_html(@depth+1)}
  2. $ }
$ end
EOF def to_html(depth=0, maxdepth=3, skip_root=false) @depth = depth @too_deep = depth >= maxdepth @skip_root = skip_root && depth == 0 TEMPLATE.expand(self) end def id entry.id end def href entry.href end def resolve(world) if Link === entry if world.include?(entry.href) target = world[entry.href] original_title = entry.text self.entry = target.entry.dup self.entry.text ||= original_title self.children = target.children + children else self.entry = Title.new(entry.text, entry.href) end resolve world end children.each { |child| child.resolve world } world end def reverse_index(index=nil) index ||= Hash.new { |h, k| h[k] = [] } children.each { |child| index[child.id] << id if child.id && id && !index[child.id].include?(id) child.reverse_index index } index end def add_to_world(world={}) world[id] = self if id children.each { |child| child.add_to_world world } world end def resolve_reverse_links(world) children.each { |child| if ReverseLink === child.entry p [:rlink, child.entry.href, entry.href] if world[child.entry.href] world[child.entry.href].children << Entry.new(Link.new(entry.href, entry.pretty), []) end else child.resolve_reverse_links world end } world end def sort! case order when :ascending children.sort! { |a, b| smartsort(a) <=> smartsort(b) } when :descending children.sort! { |a, b| smartsort(b) <=> smartsort(a) } end children.each { |child| child.sort! } self end def search(term, &block) if entry.match?(term) block.call self end children.each { |child| child.search(term, &block) } end protected def smartsort(item) nitem = item.to_s.downcase if nitem =~ /\A\d*\z/ [[0, ""], [item.to_i, item]] else nitem.scan(/\d+|\D+/).map { |part| [part.to_i, part] } end end end class Title def to_html %Q{#{text}} end def id ref || text.downcase.tr('^a-z0-9', '-').squeeze('-') end def href id end def pretty text end def match?(term) text.include?(term) || ref.include?(term) end end class Text def to_html ltext = text.gsub(/\{(\S*?)(?::(.*?))?\}/) { %Q{#{$2||$1}} } BlueCloth.new(ltext).to_html end def id Digest::MD5.hexdigest(text)[0..5] end def href id end def text_without_links text.gsub(/\{(.*?)(?::(.*?))?\}/) { $2 || $1 } end def pretty twl = text_without_links if twl.size < 30 twl else twl[0..30].gsub(/\s\S*$/, '...') end end def match?(term) text.include?(term) || id == term end end class Link def to_html %Q{#{text}} end def id nil end def pretty text end def match?(term) text.include?(term) end end class ReverseLink def to_html "" end def id nil end def pretty "" end def match?(term) false end end class Attachment def to_html if text %Q{» #{text}} else %Q{» Download #{file}.} end end def id "attachment/#{file}" end def pretty file end def match?(term) false end end def self.parse(str, id=nil) if str =~ /\A\w.*/ title = Title.new($&.chomp, id) str = $' else title = Title.new(id, id) end top = Entry.new(title, []) stack = [top] offset = 0 block = "" str.each { |line| line.gsub!("\r\n", "\n") depth = line[/^ */].size depth = offset if line =~ /^\s*$/ if depth == offset + 2 stack << stack.last.children.last elsif depth < offset while depth < offset offset -= 2 stack.pop end elsif depth > offset raise "indentation skew: #{offset} -> #{depth}: #{line.dump}" end offset = depth line.gsub!(/^\s*\|?/, '') case line when /^\s*\{(.*?)([<>]?)(?::(.*?))?\}$/ order = case $2 when "<": :ascending when ">": :descending else nil end if $1[0] == ?/ # subcategory id = stack.last.href + $1 link = Entry.new(Link.new(id, $3 || $1[1..-1]), [], order) elsif $1[0] == ?+ # reverse link link = Entry.new(ReverseLink.new($1[1..-1]), [], order) else link = Entry.new(Link.new($1, $3), [], order) end stack.last.children << Entry.new(Text.new(block), []) unless block.empty? block = "" stack.last.children << link when /^\s*$/ stack.last.children << Entry.new(Text.new(block), []) unless block.empty? block = "" else block << line end } stack.last.children << Entry.new(Text.new(block), []) unless block.empty? top end def self.load_all(dir) all = Entry.new(Top.new, []) dir = Pathname.new(dir).realpath Dir[File.join(dir, "**", "*[^~]")].each { |file| if File.file? file relfile = Pathname.new(file).relative_path_from(dir).to_s File.open(file) { |fd| header = fd.read(255) p file if header =~ /[\000-\010\013\014\016-\037\200-\377]{8,}/ || file =~ /\.(tex|rb|pl|hs|js|c|ml|pdf|cls)\Z/ entry = Entry.new(Attachment.new(relfile), []) else fd.rewind entry = parse fd.read, relfile end all.children << entry } end } all end end