resolve ambiguity between urls and email addresses
[wix.git] / src / Doc.scala
1 import edu.berkeley.sbp.scala._
2
3 import Html.mapToHtml
4 import Html.joinStrings
5 import Html.{urlEscape,htmlEscape}
6 import Html.{pre,stag,tag,stag_,tag_,stag0,link}
7
8 object Doc {
9
10   def concatMap[A,B](f: A => Seq[B], s:Seq[A]) : Seq[B] =
11     concat(s.map(f))
12
13   def concat[A](s:Seq[Seq[A]]) : Seq[A] =
14     s.foldLeft(Seq[A]())(_ ++ _)
15
16   def docFromTree(t:Tree) : Doc =
17     t match { case Tree(_,Seq(_,Tree(_,a,_)),_) => new Doc(new Header(), a.map(sectionFromTree)) }
18
19   def sectionFromTree(t:Tree) : Section =
20     t match {
21       case Tree("Section", seq,_) =>
22         seq(0) match {
23           case Tree("SectionHeader", Seq(Tree("=",e,_),c),_) =>
24             new Section(e.length-1, textSequenceFromTree(c), paragraphsFromTrees(seq.tail))
25         }
26     }
27
28   def textSequenceFromTree (t:Tree) : Seq[Text] =
29     t match {
30       case Tree("Word",    chars        ,_) => Seq(new Chars(stringFromTrees(chars)))
31       case Tree("Ordinal", x            ,_) => Seq(new Command("ordinal", Seq(new Chars(stringFromTrees(x)))))
32       case Tree("Fraction", Seq(n,d)    ,_) => Seq(new Command("fraction",Seq(new Chars(stringFromTree(n)),
33                                                                               new Chars(stringFromTree(d)))))
34       case Tree("WS",     _             ,_) => Seq(WS)
35       case Tree("Quotes", Seq(x)        ,_) => Seq(new Quotes(textSequenceFromTree(x)))
36       case Tree("Pars", y               ,_) => Seq(new SubPar(paragraphsFromTrees(y)))
37       case Tree("Command", Seq(x,y)     ,_) => Seq(new Command(stringFromTree(x), textSequenceFromTree(y)))
38       case Tree("Command",  Seq(x)      ,_) => Seq(new Command(stringFromTree(x), Seq()))
39       case Tree("Link",  Seq(text,link) ,_) => Seq(new Link(urlFromTree(link), textSequenceFromTree(text)))
40       case Tree("Footnote", x           ,_) => Seq(new Footnote(concatMap(textSequenceFromTree,x)))
41       case Tree("Keyword", x            ,_) => Seq(new Keyword(concatMap(textSequenceFromTree,x)))
42       case Tree("Math", x               ,_) => Seq(new Math(stringFromTrees(x)))
43       case Tree("Italic",  Seq(x)       ,_) => Seq(new Styled(Italic        , textSequenceFromTree(x)))
44       case Tree("Bold",  Seq(x)         ,_) => Seq(new Styled(Bold          , textSequenceFromTree(x)))
45       case Tree("Highlight",  Seq(x)    ,_) => Seq(new Styled(Highlight     , textSequenceFromTree(x)))
46       case Tree("TT", x                 ,_) => Seq(new Styled(TT            , concatMap(textSequenceFromTree,x)))
47       case Tree("Strikethrough", x      ,_) => Seq(new Styled(Strikethrough , concatMap(textSequenceFromTree,x)))
48       case Tree("Superscript", x        ,_) => Seq(new Styled(Superscript   , concatMap(textSequenceFromTree,x)))
49       case Tree("Subscript", x          ,_) => Seq(new Styled(Subscript     , concatMap(textSequenceFromTree,x)))
50       case Tree("Underline", x          ,_) => Seq(new Styled(Underline     , concatMap(textSequenceFromTree,x)))
51       case Tree("(e)",  _               ,_) => Seq(new GlyphText(Euro))
52       case Tree("(r)",  _               ,_) => Seq(new GlyphText(CircleR))
53       case Tree("(c)",  _               ,_) => Seq(new GlyphText(CircleC))
54       case Tree("(tm)", _               ,_) => Seq(new GlyphText(TradeMark))
55       case Tree("--",   _               ,_) => Seq(new GlyphText(Endash))
56       case Tree("---",  _               ,_) => Seq(new GlyphText(Emdash))
57       case Tree("<-",   _               ,_) => Seq(new GlyphText(LeftArrow))
58       case Tree("<=",   _               ,_) => Seq(new GlyphText(DoubleLeftArrow))
59       case Tree("=>",   _               ,_) => Seq(new GlyphText(DoubleRightArrow))
60       case Tree("<=>",  _               ,_) => Seq(new GlyphText(DoubleLeftRightArrow))
61       case Tree("<->",  _               ,_) => Seq(new GlyphText(LeftRightArrow))
62       case Tree("^o",   _               ,_) => Seq(new GlyphText(Degree))
63       case Tree("...",  _               ,_) => Seq(new GlyphText(Ellipsis))
64       case Tree("Text",   ts            ,_) => concat(ts.map(textSequenceFromTree))
65       case Tree("",    Seq()            ,_) => Seq()
66       case t => throw new RuntimeException("unable to create [Text] from " + t)
67     }
68
69   def hostFromTree(t:Tree) : Host =
70     t match {
71       case Tree("IP", Seq(Tree(_,a,_),Tree(_,b,_),Tree(_,c,_),Tree(_,d,_)),_) =>
72         new HostIP(intFromTrees(a), intFromTrees(b), intFromTrees(c), intFromTrees(d))
73       case Tree("DNS", parts,_) =>
74         new HostDNS(parts.map( (t:Tree) => t match { case Tree(_, c,_) => stringFromTrees(c) }))
75     }
76
77   def urlFromTree(t:Tree) : URL =
78     t match {
79       case Tree("URL", stuff,_)                              => urlFromTrees(stuff)
80       case Tree("Email", Seq(Tree("username", un,_),host),_) => new URLEmail(stringFromTrees(un), hostFromTree(host))
81       case Tree("Path",stuff,_)                              => new URLPath(stuff.map(fromUrlChar).foldLeft("")(_ + _))
82     }
83
84   def urlFromTrees(t:Seq[Tree]) : URL =
85     t match {
86       case Seq(Tree(_,method,_), login, host, port, rest @_*) =>
87         new URLNormal(stringFromTrees(method),
88                       None,
89                       hostFromTree(host),
90                       port match { case Tree("Port",port,_) => {
91                                      val q = stringFromTrees(port)
92                                      if (q.equals("")) None else Some(java.lang.Integer.parseInt(q))
93                                    }
94                                    case _ => None },
95                       rest match { case Seq(Tree("Path",p,_), x@_*) => p.map(fromUrlChar).foldLeft("")(_ + _)
96                                    case _ => "" },
97                       rest match { case Seq(_ , Tree("Path",r,_), x@_*) => Some(stringFromTrees(r))
98                                    case _ => None })
99     }
100
101   //fromUrlChar (Tree "%" [(Tree a [] _),(Tree b [] _)] _) = chr $ (fst $ head $ readHex (a++b))
102   // FIXME: problem here is the "/" vs "%2F" issue, so we "leave urls urlencoded"
103   def fromUrlChar(t:Tree) : String =
104     t match {
105       case Tree("%", Seq(Tree(a,Seq(),_),Tree(b,Seq(),_)),_) => "%"+a+b
106       case Tree(x,y,_) =>
107         if (x.length==1) x
108         else throw new RuntimeException("could not parse as URL char: " + t)
109     }
110
111   def paragraphsFromTrees(ts:Seq[Tree]) : Seq[Paragraph] =
112     consolidate(concatMap(paragraphsFromTree,ts))
113
114   def paragraphsFromTree(t:Tree) : Seq[Paragraph] =
115     consolidate (t match {
116       case Tree("Verbatim",       Seq(indent,v),_) => Seq(new P(    Seq(new Verbatim(unindent(indent,unverbate(v))))))
117       case Tree("TextParagraph",  Seq(Tree(_,text,_)),_) => Seq(new P(concatMap(textSequenceFromTree,text)))
118       case Tree("Pars",           pars       ,_) => concatMap(paragraphsFromTree,pars)
119       case Tree("HR",             _          ,_) => Seq(HR)
120       case Tree("OL",             a          ,_) =>
121         Seq(new OL(a.map( (t:Tree) => t match { case Tree("LI",x,_) => paragraphsFromTrees(x)})))
122       case Tree("UL",             a          ,_) =>
123         Seq(new UL(a.map( (t:Tree) => t match { case Tree("LI",x,_) => paragraphsFromTrees(x)})))
124       case Tree("",               _          ,_) => Seq()
125       case Tree("Blockquote",     pars       ,_) => Seq(Blockquote(paragraphsFromTrees(pars)))
126       case _ => throw new RuntimeException("unable to create [Paragraph] from " + t)
127     })
128
129   def unverbate (t:Tree) : String =
130     t match {
131       case Tree("Verbatim",x,_) => x.map(unverbate).foldLeft("")(_ + _)
132       case Tree("VerbatimBrace",Seq(x,y),_) => unverbate(x)+" "+unverbate(y)
133       case Tree(t,Seq(),_) => t
134     }
135
136   def unindent (t:Tree,v:String) : String =
137     t match {
138       case Tree("I", indent,_) => unindent_(indent.length+1, v)
139     }
140
141   private def unindent_ (i:Int,v:String) : String =
142     if (v.length==0)    ""
143     else if (v.charAt(0) == '\n') "\n"+unindent_(i, drop_(i, v.substring(1)))
144     else v.charAt(0)+unindent_(i, v.substring(1))
145
146   private def drop_(n:Int, x:String) : String = {
147     val x_ : Seq[Char] = x;
148     if (n==0) x
149     else (x_ match {
150       case Seq('\n', r@_*) => x
151       case Seq()           => ""
152       case Seq(a, b@_*)    => drop_(n-1, x.substring(1))
153     })
154   }
155
156   def consolidate(x:Seq[Paragraph]) : Seq[Paragraph] =
157     x match {
158       case Seq() => Seq()
159       case Seq(a) => Seq(a)
160       case Seq(OL(Seq()), x@_*) => consolidate(x)
161       case Seq(UL(Seq()), x@_*) => consolidate(x)
162       case Seq(OL(a),     OL(b), x@_*) => consolidate(Seq(OL(a++b))++x)
163       case Seq(UL(a),     UL(b), x@_*) => consolidate(Seq(UL(a++b))++x)
164       case Seq(a, b @_*) => Seq(a)++consolidate(b)
165     }
166
167   def intFromTrees(t:Seq[Tree]) : Int =
168     java.lang.Integer.parseInt(stringFromTrees(t))
169   def stringFromTree(t:Tree) : String =
170     t match { case Tree(h,c,_) => h++concatMap(stringFromTree,c) }
171   def stringFromTrees(ts:Seq[Tree]) : String =
172     ts.map(stringFromTree).foldLeft("")(_ + _)
173
174 }
175
176 class Doc (val header:Header, val sections:Seq[Section]) extends ToHtml {
177   override def toHtml =
178      "<!-- This document was AUTOMATICALLY GENERATED from wix source -->\n"+
179      "<!--    it is probably not a wise idea to edit it directly     -->\n\n"+
180      "<html>\n"+
181      "<head>\n"+
182      Html.style+
183      // FIXME: title tag
184      "</head>\n"+
185      "<body>\n"+   // tell jsmath we will escape stuff manually
186      Html.jsMath+  // FIXME: only put this in if math appears on the page
187      "<center><table><tr><td width=600>\n"+
188      mapToHtml(sections)+
189      "<br><br>\n"+
190      "<table width=100% class=footer><tr><td align=left>"+
191      "<img src='"+Html.printIconBase64+"'></td>"+
192      "<td align=right><span class='signature'>rendered from "+
193      "<a href=http://www.megacz.com/software/wix>"+
194      "W<span style='vertical-align:-20%'>I</span>X</a></span></div></td></tr></table>\n"+
195      "</td></tr></table></center>\n"+
196      "</body></html>"
197 }
198
199 class Header()
200
201 class Section (val level:Int, val header:Seq[Text], val paragraphs:Seq[Paragraph]) extends ToHtml {
202   def toHtml = "\n<h"+((level+1))+">\n"+(mapToHtml(header))+"\n</h"+((level+1))+">\n"+(mapToHtml(paragraphs)) }
203
204 abstract class Paragraph extends ToHtml
205   case class P          (val body :Seq[Text]                  ) extends Paragraph { override def toHtml = stag_("p",body) }
206   case object HR                                                       extends Paragraph { override def toHtml = stag("hr") }
207   case class OL         (val items:Seq[Seq[Paragraph]] ) extends Paragraph {
208     override def toHtml = stag0("ol", items.map( (s:Seq[Paragraph]) => stag_("li", s) ).foldLeft("")(_ + _)) }
209   case class UL         (val items:Seq[Seq[Paragraph]] ) extends Paragraph {
210     override def toHtml = stag0("ul", items.map( (s:Seq[Paragraph]) => stag_("li", s) ).foldLeft("")(_ + _)) }
211   case class Blockquote (val body :Seq[Paragraph]             ) extends Paragraph {
212     override def toHtml = 
213       "\n<table class=blockquote border=0 cellpadding=5px>\n"+
214       "<tr><td valign=top><image src='"+Html.quoteIconBase64+"'></td>\n"+
215       "<td class=warn>\n"+
216       mapToHtml(body)+
217       "</td></tr></table>\n"
218   }
219
220 abstract class Style
221   case object TT extends Style
222   case object Underline extends Style
223   case object Superscript extends Style
224   case object Subscript extends Style
225   case object Strikethrough extends Style
226   case object Italic extends Style
227   case object Bold extends Style
228   case object Highlight extends Style
229
230 abstract class Text extends ToHtml
231   case object WS extends Text { def toHtml = " " }
232   case class Chars(val body:String) extends Text { def toHtml = htmlEscape(body) }
233   case class Quotes(val body:Seq[Text]) extends Text { def toHtml = "&#8220;"+mapToHtml(body)+"&#8221;" }
234   case class GlyphText(val body:Glyph) extends Text { override def toHtml = body.toHtml }
235   case class Math(val body:String) extends Text { override def toHtml = "<span class=math>" +body+ "</span>" }
236   case class Verbatim(val body:String) extends Text { override def toHtml = pre(body) }
237   case class Link(val url:URL, val body:Seq[Text]) extends Text { override def toHtml = link(url.toString, body) }
238   case class Footnote(val body:Seq[Text]) extends Text { override def toHtml = throw new Exception() }
239   case class Keyword(val body:Seq[Text]) extends Text { override def toHtml = tag_("tt", body) }
240   case class SubPar(val body:Seq[Paragraph]) extends Text { override def toHtml = stag_("p", body) }
241   case class Styled(val style:Style, val body:Seq[Text]) extends Text {
242     override def toHtml =
243       style match {
244         case Underline =>       tag_("u", body)
245         case TT =>              tag_("tt", body)
246         case Italic =>          tag_("i", body)
247         case Strikethrough =>   tag_("strike", body)
248         case Superscript =>     tag_("sup", body)
249         case Subscript =>       tag_("sub", body)
250         case Bold =>            tag_("b", body)
251         case Highlight =>       "<span class=highlight>"+mapToHtml(body)+"</span>"
252       }
253   }
254   case class Command(val command:String, val body:Seq[Text]) extends Text {
255     override def toHtml = 
256       command match {
257         case "comment" =>     ""
258         case "url"     =>     "<tt>"+link(mapToHtml(body),body)+"</tt>"
259         case "WiX"     =>     "W<span style='vertical-align:-20%'>I</span>X"
260         case "TeX"     =>     "T<span style='vertical-align:-20%'>E</span>X"
261         case "red"     =>     "<font color=red>"+mapToHtml(body)+"</font>"
262         case "orange"  =>     "<font color=orange>"+mapToHtml(body)+"</font>"
263         case "green"   =>     "<font color=green>"+mapToHtml(body)+"</font>"
264         case "sc"      =>     "<sc>"+mapToHtml(body)+"</sc>"
265         case "image"   =>     "<img src='"+mapToHtml(body)+"'/>"
266         case "imagec"  =>     "<center><img src='"+mapToHtml(body)+"'/></center>"
267         case "image2"  =>     "<img width=180px src='"+mapToHtml(body)+"'/>"
268         case "image3"  =>     "<img width=200px src='"+mapToHtml(body)+"'/>"
269         case "image4"  =>     "<center><img width=550px src='"+mapToHtml(body)+"'/></center>"
270         case "warn"    =>     "\n<div class=warn>\n<table border=0 cellpadding=5px>\n"+
271                               "<tr><td valign=top><image src='"+Html.warnIconBase64+"'></td>\n"+
272                               "<td class=warn>\n"+
273                               mapToHtml(body)+
274                               "</td></tr></table></div>\n"
275         case "announce" =>    "\n<div class=announce>\n<table border=0 cellpadding=5px>\n"+
276                               "<tr><td valign=top></td>\n"+
277                               "<td class=warn>\n"+
278                               mapToHtml(body)+
279                               "</td></tr></table></div>\n"
280         case "br"      =>     "\n<br/>\n"
281         case "alpha"   =>     "&#x03B1;"
282         case "beta"    =>     "&#x03B2;"
283         case "gamma"   =>     "&#x03B3;"
284         case "delta"   =>     "&#x03B4;"
285         case "epsilon" =>     "&#x03B5;"
286         case "eta"     =>     "&#x03B7;"
287         case "theta"   =>     "&#x03B8;"
288         case "kappa"   =>     "&#x03Ba;"
289         case "lambda"  =>     "&#x03Bb;"
290         case "mu"      =>     "&#x03Bc;"
291         case "nu"      =>     "&#x03Bd;"
292         case "pi"      =>     "&#x03c0;"
293         case "rho"     =>     "&#x03c1;"
294 //25a1
295         case "Box"     =>     "&#x20de;"
296         // TO DO: integrate stixfonts stuff
297         case "cent"    =>     "&#189;"
298         case "euro"    =>     "&#8364;"
299         // gross hack
300         case "ordinal"     => {
301           val x = mapToHtml(body)
302           if      (x.charAt(x.length-1) == '1') x+"<sup>"+"st"+"</sup>"
303           else if (x.charAt(x.length-1) == '2') x+"<sup>"+"nd"+"</sup>"
304           else if (x.charAt(x.length-1) == '3') x+"<sup>"+"rd"+"</sup>"
305           else                                  x+"<sup>"+"th"+"</sup>"
306         }
307         // TO DO: use "unicode vulgar fractions" here
308         // directional quotes: see http://www.dwheeler.com/essays/quotes-in-html.html
309         /*    u'1/2' : u'\u00BD',
310         //    u'1/4' : u'\u00BC',
311         //    u'3/4' : u'\u00BE',
312         //    u'1/3' : u'\u2153',
313         //    u'2/3' : u'\u2154',
314         //    u'1/5' : u'\u2155',
315         //    u'2/5' : u'\u2156',
316         //    u'3/5' : u'\u2157',
317         //    u'4/5' : u'\u2158',
318         //    u'1/6' : u'\u2159',
319         //    u'5/6' : u'\u215A',
320         //    u'1/8' : u'\u215B',
321         //    u'3/8' : u'\u215C',
322         //    u'5/8' : u'\u215D',
323         //    u'7/8' : u'\u215E',
324         */
325         case "fraction"           => "<sup>"++body(0).toHtml++"</sup>"++"/"++"<sub>"++body(1).toHtml++"</sub>"
326         case "rfc"                => "<tt><a href=http://tools.ietf.org/html/rfc"+mapToHtml(body)+">RFC"+mapToHtml(body)+"</a></tt>"
327         case "keystroke:command"  => "&#x2318;"
328         case "keystroke:shift"    => "&#x21E7;"
329         case "keystroke:option"   => "&#x2325;"
330         case "keystroke:control"  => "&#x2303;"
331         case "keystroke:capslock" => "&#x21EA;"
332         case "keystroke:apple"    => "&#xF8FF;"
333         case _                    => throw new RuntimeException("unsupported command " + command)
334       }
335   }
336
337
338 abstract class Glyph extends ToHtml
339   case object Euro extends Glyph { override def toHtml = "&#8364;" }
340   case object CircleR extends Glyph { override def toHtml = "&#162;" }
341   case object CircleC extends Glyph { override def toHtml = "&#174;" }
342   case object TradeMark extends Glyph { override def toHtml = "&#8482;" }
343   case object ServiceMark extends Glyph { override def toHtml = "&#8482;" }
344   case object Endash extends Glyph { override def toHtml = "&ndash;" }
345   case object Emdash extends Glyph { override def toHtml = "&mdash;" }
346   case object Ellipsis extends Glyph { override def toHtml = "&#0133;"  /* &cdots;? */ }
347   case object Cent extends Glyph { override def toHtml = "&#189;" }
348   case object Daggar extends Glyph { override def toHtml = "&#8224;" }
349   case object DoubleDaggar extends Glyph { override def toHtml = "&#8225;" }
350   case object Clover extends Glyph { override def toHtml = "&#8984;" }
351   case object Flat extends Glyph { override def toHtml = "&#8918;" }
352   case object Natural extends Glyph { override def toHtml = "&#8919;" }
353   case object Sharp extends Glyph { override def toHtml = "&#8920;" }
354   case object CheckMark extends Glyph { override def toHtml = "&#10003;" }
355   case object XMark extends Glyph { override def toHtml = "&#10007;" }
356   case object LeftArrow extends Glyph { override def toHtml = "&larr;" }
357   case object RightArrow extends Glyph { override def toHtml = "&rarr;" }
358   case object DoubleLeftArrow extends Glyph { override def toHtml = "&#;"  /* FIXME */ }
359   case object DoubleRightArrow extends Glyph { override def toHtml = "&#;"  /* FIXME */ }
360   case object DoubleLeftRightArrow extends Glyph { override def toHtml = "&#;"  /* FIXME */ }
361   case object LeftRightArrow extends Glyph { override def toHtml = "&#;"  /* FIXME */ }
362   case object Degree extends Glyph { override def toHtml = "&#;"  /* FIXME */ }
363
364 class Login(val user:String, val password:Option[String]) {
365   override def toString =
366     password match {
367       case None    => user
368       case Some(x) => user+":"+urlEscape(x) }
369 }
370
371 abstract class URL
372   case class URLPath(val path:String) extends URL { override def toString = path }
373   case class URLEmail(val user:String, val host:Host) extends URL { override def toString = "mailto:"+user+"@"+host }
374   case class URLNormal(val method:String,
375                        val login:Option[Login],
376                        val host:Host,
377                        val port:Option[Int],
378                        val path:String,
379                        val ref:Option[String]) extends URL {
380     override def toString =
381      method+"://"+
382      (login match {
383        case None => ""
384        case Some(log) => log+"@" })+
385      host+
386      "/"+
387      path+
388      (ref match {
389        case None => ""
390        case Some("") => ""
391        case Some(x) => "#"+x })
392
393   }
394
395
396 abstract class Host
397   case class HostIP(val ip0:Int, val ip1:Int, val ip2:Int, val ip3:Int) extends Host {
398     override def toString = ip0+"."+ip1+"."+ip2+"."+ip3 }
399   case class HostDNS(val parts:Seq[String]) extends Host {
400     override def toString = joinStrings(parts, ".") }
401
402
403
404
405
406
407
408
409
410
411
412
413
414