| author | desharna | 
| Sat, 15 Mar 2025 20:17:03 +0100 | |
| changeset 82282 | 919eb0e67930 | 
| parent 81668 | ca4ecbbdd728 | 
| child 83203 | 61277d1550d6 | 
| permissions | -rw-r--r-- | 
| 79498 | 1 | /* Title: Pure/General/html.scala | 
| 33984 | 2 | Author: Makarius | 
| 3 | ||
| 50707 
5b2bf7611662
maintain session index on Scala side, for more determistic results;
 wenzelm parents: 
50201diff
changeset | 4 | HTML presentation elements. | 
| 33984 | 5 | */ | 
| 6 | ||
| 7 | package isabelle | |
| 8 | ||
| 55618 | 9 | |
| 81668 | 10 | import java.awt.Color | 
| 11 | import java.util.Locale | |
| 12 | ||
| 77620 | 13 | import org.jsoup.nodes.{Entities => Jsoup_Entities, Document => Jsoup_Document}
 | 
| 14 | import org.jsoup.Jsoup | |
| 15 | ||
| 16 | ||
| 75393 | 17 | object HTML {
 | 
| 81659 | 18 | /* spaces (non-breaking) */ | 
| 19 | ||
| 20 | val space = "\u00a0" | |
| 21 | ||
| 22 | private val static_spaces = space * 100 | |
| 23 | ||
| 24 |   def spaces(n: Int): String = {
 | |
| 25 | require(n >= 0, "negative spaces") | |
| 26 | if (n == 0) "" | |
| 27 | else if (n < static_spaces.length) static_spaces.substring(0, n) | |
| 28 | else space * n | |
| 29 | } | |
| 30 | ||
| 31 | ||
| 73207 | 32 | /* attributes */ | 
| 33 | ||
| 75393 | 34 |   class Attribute(val name: String, value: String) {
 | 
| 73207 | 35 | def xml: XML.Attribute = name -> value | 
| 36 | def apply(elem: XML.Elem): XML.Elem = elem + xml | |
| 37 | } | |
| 38 | ||
| 39 |   def id(s: String): Attribute = new Attribute("id", s)
 | |
| 40 |   def class_(name: String): Attribute = new Attribute("class", name)
 | |
| 41 | ||
| 42 |   def width(w: Int): Attribute = new Attribute("width", w.toString)
 | |
| 43 |   def height(h: Int): Attribute = new Attribute("height", h.toString)
 | |
| 44 | def size(w: Int, h: Int)(elem: XML.Elem): XML.Elem = width(w)(height(h)(elem)) | |
| 45 | ||
| 74679 | 46 |   val entity_def: Attribute = class_("entity_def")
 | 
| 47 |   val entity_ref: Attribute = class_("entity_ref")
 | |
| 48 | ||
| 73207 | 49 | |
| 50 | /* structured markup operators */ | |
| 51 | ||
| 52 | def text(txt: String): XML.Body = if (txt.isEmpty) Nil else List(XML.Text(txt)) | |
| 53 |   val break: XML.Body = List(XML.elem("br"))
 | |
| 54 |   val nl: XML.Body = List(XML.Text("\n"))
 | |
| 55 | ||
| 75393 | 56 |   class Operator(val name: String) {
 | 
| 73207 | 57 | def apply(body: XML.Body): XML.Elem = XML.elem(name, body) | 
| 58 | def apply(att: Attribute, body: XML.Body): XML.Elem = att(apply(body)) | |
| 59 | def apply(c: String, body: XML.Body): XML.Elem = apply(class_(c), body) | |
| 60 | } | |
| 61 | ||
| 75393 | 62 |   class Heading(name: String) extends Operator(name) {
 | 
| 73207 | 63 | def apply(txt: String): XML.Elem = super.apply(text(txt)) | 
| 64 | def apply(att: Attribute, txt: String): XML.Elem = super.apply(att, text(txt)) | |
| 65 | def apply(c: String, txt: String): XML.Elem = super.apply(c, text(txt)) | |
| 66 | } | |
| 67 | ||
| 68 |   val div = new Operator("div")
 | |
| 69 |   val span = new Operator("span")
 | |
| 70 |   val pre = new Operator("pre")
 | |
| 71 |   val par = new Operator("p")
 | |
| 72 |   val sub = new Operator("sub")
 | |
| 73 |   val sup = new Operator("sup")
 | |
| 74 |   val emph = new Operator("em")
 | |
| 75 |   val bold = new Operator("b")
 | |
| 76 |   val code = new Operator("code")
 | |
| 77 |   val item = new Operator("li")
 | |
| 78 |   val list = new Operator("ul")
 | |
| 74657 
9fcf80ceb863
updated to scala-2.13.7 --- problems with jline disappear after purging $HOME/.inputrc;
 wenzelm parents: 
73340diff
changeset | 79 |   val `enum` = new Operator("ol")
 | 
| 73207 | 80 |   val descr = new Operator("dl")
 | 
| 81 |   val dt = new Operator("dt")
 | |
| 82 |   val dd = new Operator("dd")
 | |
| 83 | ||
| 84 |   val title = new Heading("title")
 | |
| 85 |   val chapter = new Heading("h1")
 | |
| 86 |   val section = new Heading("h2")
 | |
| 87 |   val subsection = new Heading("h3")
 | |
| 88 |   val subsubsection = new Heading("h4")
 | |
| 89 |   val paragraph = new Heading("h5")
 | |
| 90 |   val subparagraph = new Heading("h6")
 | |
| 91 | ||
| 92 | def itemize(items: List[XML.Body]): XML.Elem = list(items.map(item(_))) | |
| 74657 
9fcf80ceb863
updated to scala-2.13.7 --- problems with jline disappear after purging $HOME/.inputrc;
 wenzelm parents: 
73340diff
changeset | 93 | def enumerate(items: List[XML.Body]): XML.Elem = `enum`(items.map(item(_))) | 
| 73207 | 94 | def description(items: List[(XML.Body, XML.Body)]): XML.Elem = | 
| 95 |     descr(items.flatMap({ case (x, y) => List(dt(x), dd(y)) }))
 | |
| 96 | ||
| 97 | def link(href: String, body: XML.Body): XML.Elem = | |
| 98 |     XML.Elem(Markup("a", List("href" -> href)), if (body.isEmpty) text(href) else body)
 | |
| 99 | ||
| 100 | def link(path: Path, body: XML.Body): XML.Elem = link(path.implode, body) | |
| 101 | ||
| 102 | def image(src: String, alt: String = ""): XML.Elem = | |
| 103 |     XML.Elem(Markup("img", List("src" -> src) ::: proper_string(alt).map("alt" -> _).toList), Nil)
 | |
| 104 | ||
| 105 |   def source(body: XML.Body): XML.Elem = pre("source", body)
 | |
| 106 | def source(src: String): XML.Elem = source(text(src)) | |
| 107 | ||
| 108 |   def style(s: String): XML.Elem = XML.elem("style", text(s))
 | |
| 109 | def style_file(href: String): XML.Elem = | |
| 110 |     XML.Elem(Markup("link", List("rel" -> "stylesheet", "type" -> "text/css", "href" -> href)), Nil)
 | |
| 111 | def style_file(path: Path): XML.Elem = style_file(Url.print_file(path.file)) | |
| 112 | ||
| 80203 
ca9a402735b4
proper html script tag: source code must not be escaped;
 Fabian Huch <huch@in.tum.de> parents: 
80018diff
changeset | 113 |   def script(s: String): XML.Elem = XML.elem("script", List(raw(text(s))))
 | 
| 73207 | 114 |   def script_file(href: String): XML.Elem = XML.Elem(Markup("script", List("src" -> href)), Nil)
 | 
| 115 | def script_file(path: Path): XML.Elem = script_file(Url.print_file(path.file)) | |
| 116 | ||
| 81668 | 117 |   def color(c: Color): String = {
 | 
| 118 | val r = Integer.valueOf(c.getRed) | |
| 119 | val g = Integer.valueOf(c.getGreen) | |
| 120 | val b = Integer.valueOf(c.getBlue) | |
| 121 |     c.getAlpha match {
 | |
| 122 | case 255 => String.format(Locale.ROOT, "rgb(%d,%d,%d)", r, g, b) | |
| 123 | case a => | |
| 124 | String.format(Locale.ROOT, "rgba(%d,%d,%d,%.2f)", r, g, b, | |
| 125 | java.lang.Double.valueOf(a.toDouble / 255)) | |
| 126 | } | |
| 127 | } | |
| 128 | ||
| 129 | def color_property(c: Color): String = "color: " + color(c) | |
| 130 | def background_property(c: Color): String = "background: " + color(c) | |
| 131 | ||
| 73207 | 132 | |
| 75933 | 133 | /* href */ | 
| 134 | ||
| 75940 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 135 |   def relative_href(location: Path, base: Option[Path] = None): String = {
 | 
| 75939 | 136 | val path = | 
| 137 |       base match {
 | |
| 138 | case None => | |
| 75940 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 139 | val path = location.expand | 
| 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 140 |           if (path.is_absolute) Exn.error("Relative href location expected: " + path) else path
 | 
| 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 141 | case Some(base_dir) => | 
| 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 142 | val path1 = base_dir.absolute_file.toPath | 
| 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 143 | val path2 = location.absolute_file.toPath | 
| 75939 | 144 |           try { File.path(path1.relativize(path2).toFile) }
 | 
| 145 |           catch {
 | |
| 146 | case _: IllegalArgumentException => | |
| 75940 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 147 |               Exn.error("Failed to relativize href location " + path2 + " with wrt. base " + path1)
 | 
| 75939 | 148 | } | 
| 149 | } | |
| 150 | if (path.is_current) "" else path.implode | |
| 75933 | 151 | } | 
| 152 | ||
| 153 | ||
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 154 | /* output text with control symbols */ | 
| 59063 | 155 | |
| 73208 | 156 | private val control: Map[Symbol.Symbol, Operator] = | 
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 157 | Map( | 
| 73206 | 158 | Symbol.sub -> sub, Symbol.sub_decoded -> sub, | 
| 159 | Symbol.sup -> sup, Symbol.sup_decoded -> sup, | |
| 160 | Symbol.bold -> bold, Symbol.bold_decoded -> bold) | |
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 161 | |
| 73208 | 162 | private val control_block_begin: Map[Symbol.Symbol, Operator] = | 
| 62104 
fb73c0d7bb37
clarified symbol insertion, depending on buffer encoding;
 wenzelm parents: 
60215diff
changeset | 163 | Map( | 
| 73206 | 164 | Symbol.bsub -> sub, Symbol.bsub_decoded -> sub, | 
| 165 | Symbol.bsup -> sup, Symbol.bsup_decoded -> sup) | |
| 166 | ||
| 73208 | 167 | private val control_block_end: Map[Symbol.Symbol, Operator] = | 
| 73206 | 168 | Map( | 
| 169 | Symbol.esub -> sub, Symbol.esub_decoded -> sub, | |
| 170 | Symbol.esup -> sup, Symbol.esup_decoded -> sup) | |
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 171 | |
| 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 172 | def is_control(sym: Symbol.Symbol): Boolean = control.isDefinedAt(sym) | 
| 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 173 | |
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 174 | def is_control_block_begin(sym: Symbol.Symbol): Boolean = control_block_begin.isDefinedAt(sym) | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 175 | def is_control_block_end(sym: Symbol.Symbol): Boolean = control_block_end.isDefinedAt(sym) | 
| 75393 | 176 |   def is_control_block_pair(bg: Symbol.Symbol, en: Symbol.Symbol): Boolean = {
 | 
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 177 | val bg_decoded = Symbol.decode(bg) | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 178 | val en_decoded = Symbol.decode(en) | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 179 | bg_decoded == Symbol.bsub_decoded && en_decoded == Symbol.esub_decoded || | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 180 | bg_decoded == Symbol.bsup_decoded && en_decoded == Symbol.esup_decoded | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 181 | } | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 182 | |
| 75393 | 183 |   def check_control_blocks(body: XML.Body): Boolean = {
 | 
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 184 | var ok = true | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 185 | var open = List.empty[Symbol.Symbol] | 
| 78592 | 186 |     for { case XML.Text(text) <- body; sym <- Symbol.iterator(text) } {
 | 
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 187 | if (is_control_block_begin(sym)) open ::= sym | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 188 |       else if (is_control_block_end(sym)) {
 | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 189 |         open match {
 | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 190 | case bg :: rest if is_control_block_pair(bg, sym) => open = rest | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 191 | case _ => ok = false | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 192 | } | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 193 | } | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 194 | } | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 195 | ok && open.isEmpty | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 196 | } | 
| 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 197 | |
| 75393 | 198 | def output( | 
| 199 | s: StringBuilder, | |
| 200 | text: String, | |
| 201 | control_blocks: Boolean, | |
| 202 | hidden: Boolean, | |
| 203 | permissive: Boolean | |
| 204 |   ): Unit = {
 | |
| 80429 | 205 | val xml_output = new XML.Output(s) | 
| 206 | ||
| 66018 
9ce3720976dc
permissive output of XML.Text, e.g. relevant for embedded <style>;
 wenzelm parents: 
66006diff
changeset | 207 | def output_string(str: String): Unit = | 
| 80429 | 208 | xml_output.string(str, permissive = permissive) | 
| 66018 
9ce3720976dc
permissive output of XML.Text, e.g. relevant for embedded <style>;
 wenzelm parents: 
66006diff
changeset | 209 | |
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 210 | def output_hidden(body: => Unit): Unit = | 
| 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 211 |       if (hidden) { s ++= "<span class=\"hidden\">"; body; s ++= "</span>" }
 | 
| 37200 
0f3edc64356a
added HTML.encode (in Scala), similar to HTML.output in ML;
 wenzelm parents: 
36016diff
changeset | 212 | |
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 213 | def output_symbol(sym: Symbol.Symbol): Unit = | 
| 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 214 |       if (sym != "") {
 | 
| 73206 | 215 |         control_block_begin.get(sym) match {
 | 
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 216 | case Some(op) if control_blocks => | 
| 73206 | 217 | output_hidden(output_string(sym)) | 
| 80429 | 218 | xml_output.elem(Markup(op.name, Nil)) | 
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 219 | case _ => | 
| 73206 | 220 |             control_block_end.get(sym) match {
 | 
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 221 | case Some(op) if control_blocks => | 
| 80429 | 222 | xml_output.end_elem(op.name) | 
| 73206 | 223 | output_hidden(output_string(sym)) | 
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 224 | case _ => | 
| 73206 | 225 |                 if (hidden && Symbol.is_control_encoded(sym)) {
 | 
| 226 | output_hidden(output_string(Symbol.control_prefix)) | |
| 227 | s ++= "<span class=\"control\">" | |
| 228 | output_string(Symbol.control_name(sym).get) | |
| 229 | s ++= "</span>" | |
| 230 | output_hidden(output_string(Symbol.control_suffix)) | |
| 231 | } | |
| 232 | else output_string(sym) | |
| 67255 
f1f983484878
HTML rendering of \<^control> as in Isabelle/jEdit;
 wenzelm parents: 
66212diff
changeset | 233 | } | 
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 234 | } | 
| 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 235 | } | 
| 59063 | 236 | |
| 62104 
fb73c0d7bb37
clarified symbol insertion, depending on buffer encoding;
 wenzelm parents: 
60215diff
changeset | 237 | var ctrl = "" | 
| 59063 | 238 |     for (sym <- Symbol.iterator(text)) {
 | 
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 239 |       if (is_control(sym)) { output_symbol(ctrl); ctrl = sym }
 | 
| 59063 | 240 |       else {
 | 
| 62104 
fb73c0d7bb37
clarified symbol insertion, depending on buffer encoding;
 wenzelm parents: 
60215diff
changeset | 241 |         control.get(ctrl) match {
 | 
| 73206 | 242 | case Some(op) if Symbol.is_controllable(sym) => | 
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 243 | output_hidden(output_symbol(ctrl)) | 
| 80429 | 244 | xml_output.elem(Markup(op.name, Nil)) | 
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 245 | output_symbol(sym) | 
| 80429 | 246 | xml_output.end_elem(op.name) | 
| 59063 | 247 | case _ => | 
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 248 | output_symbol(ctrl) | 
| 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 249 | output_symbol(sym) | 
| 59063 | 250 | } | 
| 62104 
fb73c0d7bb37
clarified symbol insertion, depending on buffer encoding;
 wenzelm parents: 
60215diff
changeset | 251 | ctrl = "" | 
| 59063 | 252 | } | 
| 37200 
0f3edc64356a
added HTML.encode (in Scala), similar to HTML.output in ML;
 wenzelm parents: 
36016diff
changeset | 253 | } | 
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 254 | output_symbol(ctrl) | 
| 64355 | 255 | } | 
| 59063 | 256 | |
| 75393 | 257 |   def output(text: String): String = {
 | 
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 258 | val control_blocks = check_control_blocks(List(XML.Text(text))) | 
| 80440 | 259 |     Library.string_builder() { builder =>
 | 
| 260 | output(builder, text, control_blocks = control_blocks, hidden = false, permissive = true) | |
| 261 | } | |
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 262 | } | 
| 64355 | 263 | |
| 264 | ||
| 265 | /* output XML as HTML */ | |
| 266 | ||
| 65834 
67a6e0f166c2
extra space only for some structual elements, but not <a>, <b>, <em> etc. (amending 8a0fe5469ba0);
 wenzelm parents: 
65773diff
changeset | 267 | private val structural_elements = | 
| 
67a6e0f166c2
extra space only for some structual elements, but not <a>, <b>, <em> etc. (amending 8a0fe5469ba0);
 wenzelm parents: 
65773diff
changeset | 268 |     Set("head", "body", "meta", "div", "pre", "p", "title", "h1", "h2", "h3", "h4", "h5", "h6",
 | 
| 
67a6e0f166c2
extra space only for some structual elements, but not <a>, <b>, <em> etc. (amending 8a0fe5469ba0);
 wenzelm parents: 
65773diff
changeset | 269 | "ul", "ol", "dl", "li", "dt", "dd") | 
| 
67a6e0f166c2
extra space only for some structual elements, but not <a>, <b>, <em> etc. (amending 8a0fe5469ba0);
 wenzelm parents: 
65773diff
changeset | 270 | |
| 75393 | 271 |   def output(s: StringBuilder, xml: XML.Body, hidden: Boolean, structural: Boolean): Unit = {
 | 
| 80429 | 272 | val xml_output = new XML.Output(s) | 
| 273 | ||
| 75393 | 274 |     def output_body(body: XML.Body): Unit = {
 | 
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 275 | val control_blocks = check_control_blocks(body) | 
| 73205 | 276 |       body foreach {
 | 
| 64355 | 277 | case XML.Elem(markup, Nil) => | 
| 80429 | 278 | xml_output.elem(markup, end = true) | 
| 80018 
ac4412562c7b
more robust XML body: allow empty text, as well as arbitrary pro-forma markup (e.g. see XML.blob in Isabelle/ML);
 wenzelm parents: 
80017diff
changeset | 279 | case XML.Elem(Markup(Markup.RAW_HTML, _), body) => | 
| 80439 | 280 | XML.traverse_text(body, (), (_, raw) => s.append(raw)) | 
| 64355 | 281 | case XML.Elem(markup, ts) => | 
| 67337 
4254cfd15b00
more tight HTML output: avoid extra lines within <pre>;
 wenzelm parents: 
67336diff
changeset | 282 | if (structural && structural_elements(markup.name)) s += '\n' | 
| 80429 | 283 | xml_output.elem(markup) | 
| 73205 | 284 | output_body(ts) | 
| 80429 | 285 | xml_output.end_elem(markup.name) | 
| 67337 
4254cfd15b00
more tight HTML output: avoid extra lines within <pre>;
 wenzelm parents: 
67336diff
changeset | 286 | if (structural && structural_elements(markup.name)) s += '\n' | 
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 287 | case XML.Text(txt) => | 
| 73209 
de16d797adbe
more robust HTML output: non-balanced bsub/esub are shown verbatim, e.g. within mixfix declarations (due to extra markup);
 wenzelm parents: 
73208diff
changeset | 288 | output(s, txt, control_blocks = control_blocks, hidden = hidden, permissive = true) | 
| 64355 | 289 | } | 
| 73205 | 290 | } | 
| 291 | output_body(xml) | |
| 37200 
0f3edc64356a
added HTML.encode (in Scala), similar to HTML.output in ML;
 wenzelm parents: 
36016diff
changeset | 292 | } | 
| 
0f3edc64356a
added HTML.encode (in Scala), similar to HTML.output in ML;
 wenzelm parents: 
36016diff
changeset | 293 | |
| 67337 
4254cfd15b00
more tight HTML output: avoid extra lines within <pre>;
 wenzelm parents: 
67336diff
changeset | 294 | def output(body: XML.Body, hidden: Boolean, structural: Boolean): String = | 
| 80440 | 295 |     Library.string_builder(hint = XML.text_length(body) * 2) { builder =>
 | 
| 296 | output(builder, body, hidden, structural) | |
| 297 | } | |
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 298 | |
| 67337 
4254cfd15b00
more tight HTML output: avoid extra lines within <pre>;
 wenzelm parents: 
67336diff
changeset | 299 | def output(tree: XML.Tree, hidden: Boolean, structural: Boolean): String = | 
| 
4254cfd15b00
more tight HTML output: avoid extra lines within <pre>;
 wenzelm parents: 
67336diff
changeset | 300 | output(List(tree), hidden, structural) | 
| 64355 | 301 | |
| 302 | ||
| 77620 | 303 | /* input */ | 
| 74921 | 304 | |
| 80018 
ac4412562c7b
more robust XML body: allow empty text, as well as arbitrary pro-forma markup (e.g. see XML.blob in Isabelle/ML);
 wenzelm parents: 
80017diff
changeset | 305 | def input_raw(text: String): XML.Elem = raw(HTML.text(input(text))) | 
| 77620 | 306 | def input(text: String): String = Jsoup_Entities.unescape(text) | 
| 80018 
ac4412562c7b
more robust XML body: allow empty text, as well as arbitrary pro-forma markup (e.g. see XML.blob in Isabelle/ML);
 wenzelm parents: 
80017diff
changeset | 307 | def raw(body: XML.Body): XML.Elem = XML.elem(Markup.RAW_HTML, body) | 
| 77620 | 308 | |
| 309 | def parse_document(html: String): Jsoup_Document = Jsoup.parse(html) | |
| 310 | def get_document(url: String): Jsoup_Document = Jsoup.connect(url).get() | |
| 74921 | 311 | |
| 312 | ||
| 65892 | 313 | /* messages */ | 
| 314 | ||
| 65939 | 315 | // background | 
| 65993 | 316 |   val writeln_message: Attribute = class_("writeln_message")
 | 
| 317 |   val warning_message: Attribute = class_("warning_message")
 | |
| 318 |   val error_message: Attribute = class_("error_message")
 | |
| 65892 | 319 | |
| 65939 | 320 | // underline | 
| 65993 | 321 |   val writeln: Attribute = class_("writeln")
 | 
| 322 |   val warning: Attribute = class_("warning")
 | |
| 323 |   val error: Attribute = class_("error")
 | |
| 65892 | 324 | |
| 65944 | 325 | |
| 326 | /* tooltips */ | |
| 327 | ||
| 328 | def tooltip(item: XML.Body, tip: XML.Body): XML.Elem = | |
| 65992 | 329 |     span(item ::: List(div("tooltip", tip)))
 | 
| 65944 | 330 | |
| 331 | def tooltip_errors(item: XML.Body, msgs: List[XML.Body]): XML.Elem = | |
| 332 | HTML.error(tooltip(item, msgs.map(msg => error_message(pre(msg))))) | |
| 65892 | 333 | |
| 334 | ||
| 66196 | 335 | /* GUI elements */ | 
| 336 | ||
| 75393 | 337 |   object GUI {
 | 
| 66196 | 338 | private def optional_value(text: String): XML.Attributes = | 
| 339 | proper_string(text).map(a => "value" -> a).toList | |
| 340 | ||
| 341 | private def optional_name(name: String): XML.Attributes = | |
| 342 | proper_string(name).map(a => "name" -> a).toList | |
| 343 | ||
| 344 | private def optional_title(tooltip: String): XML.Attributes = | |
| 345 | proper_string(tooltip).map(a => "title" -> a).toList | |
| 346 | ||
| 347 | private def optional_submit(submit: Boolean): XML.Attributes = | |
| 348 |       if (submit) List("onChange" -> "this.form.submit()") else Nil
 | |
| 349 | ||
| 350 | private def optional_checked(selected: Boolean): XML.Attributes = | |
| 351 |       if (selected) List("checked" -> "") else Nil
 | |
| 352 | ||
| 353 | private def optional_action(action: String): XML.Attributes = | |
| 354 | proper_string(action).map(a => "action" -> a).toList | |
| 355 | ||
| 66210 | 356 | private def optional_onclick(script: String): XML.Attributes = | 
| 357 | proper_string(script).map(a => "onclick" -> a).toList | |
| 358 | ||
| 359 | private def optional_onchange(script: String): XML.Attributes = | |
| 360 | proper_string(script).map(a => "onchange" -> a).toList | |
| 361 | ||
| 362 | def button(body: XML.Body, name: String = "", tooltip: String = "", submit: Boolean = false, | |
| 363 | script: String = ""): XML.Elem = | |
| 66196 | 364 | XML.Elem( | 
| 365 |         Markup("button",
 | |
| 366 |           List("type" -> (if (submit) "submit" else "button"), "value" -> "true") :::
 | |
| 66210 | 367 | optional_name(name) ::: optional_title(tooltip) ::: optional_onclick(script)), body) | 
| 66196 | 368 | |
| 369 | def checkbox(body: XML.Body, name: String = "", tooltip: String = "", submit: Boolean = false, | |
| 66210 | 370 | selected: Boolean = false, script: String = ""): XML.Elem = | 
| 66196 | 371 |       XML.elem("label",
 | 
| 372 | XML.elem( | |
| 373 |           Markup("input",
 | |
| 374 |             List("type" -> "checkbox", "value" -> "true") ::: optional_name(name) :::
 | |
| 375 | optional_title(tooltip) ::: optional_submit(submit) ::: | |
| 66210 | 376 | optional_checked(selected) ::: optional_onchange(script))) :: body) | 
| 66196 | 377 | |
| 378 | def text_field(columns: Int = 0, text: String = "", name: String = "", tooltip: String = "", | |
| 66210 | 379 | submit: Boolean = false, script: String = ""): XML.Elem = | 
| 66196 | 380 |       XML.elem(Markup("input",
 | 
| 381 |         List("type" -> "text") :::
 | |
| 382 |           (if (columns > 0) List("size" -> columns.toString) else Nil) :::
 | |
| 66210 | 383 | optional_value(text) ::: optional_name(name) ::: optional_title(tooltip) ::: | 
| 384 | optional_submit(submit) ::: optional_onchange(script))) | |
| 66196 | 385 | |
| 386 | def parameter(text: String = "", name: String = ""): XML.Elem = | |
| 387 | XML.elem( | |
| 388 |         Markup("input", List("type" -> "hidden") ::: optional_value(text) ::: optional_name(name)))
 | |
| 389 | ||
| 66209 | 390 | def form(body: XML.Body, name: String = "", action: String = "", http_post: Boolean = false) | 
| 391 | : XML.Elem = | |
| 392 | XML.Elem( | |
| 393 |         Markup("form", optional_name(name) ::: optional_action(action) :::
 | |
| 394 |           (if (http_post) List("method" -> "post") else Nil)), body)
 | |
| 66196 | 395 | } | 
| 396 | ||
| 397 | ||
| 66200 | 398 | /* GUI layout */ | 
| 399 | ||
| 75393 | 400 |   object Wrap_Panel {
 | 
| 78603 | 401 |     enum Alignment { case left, right, center }
 | 
| 66200 | 402 | |
| 75393 | 403 | def apply( | 
| 404 | contents: List[XML.Elem], | |
| 405 | name: String = "", | |
| 406 | action: String = "", | |
| 78603 | 407 | alignment: Alignment = Alignment.right | 
| 75393 | 408 |     ): XML.Elem = {
 | 
| 66200 | 409 |       val body = Library.separate(XML.Text(" "), contents)
 | 
| 410 |       GUI.form(List(div(body) + ("style" -> ("text-align: " + alignment))),
 | |
| 411 | name = name, action = action) | |
| 412 | } | |
| 413 | } | |
| 414 | ||
| 415 | ||
| 51399 
6ac3c29a300e
discontinued "isabelle usedir" option -r (reset session path);
 wenzelm parents: 
50707diff
changeset | 416 | /* document */ | 
| 
6ac3c29a300e
discontinued "isabelle usedir" option -r (reset session path);
 wenzelm parents: 
50707diff
changeset | 417 | |
| 64359 | 418 | val header: String = | 
| 69804 | 419 | XML.header + | 
| 420 | """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
| 64359 | 421 | <html xmlns="http://www.w3.org/1999/xhtml">""" | 
| 422 | ||
| 73129 | 423 | val footer: String = """</html>""" | 
| 424 | ||
| 64359 | 425 | val head_meta: XML.Elem = | 
| 426 |     XML.Elem(Markup("meta",
 | |
| 427 |       List("http-equiv" -> "Content-Type", "content" -> "text/html; charset=utf-8")), Nil)
 | |
| 428 | ||
| 75393 | 429 | def output_document( | 
| 430 | head: XML.Body, | |
| 431 | body: XML.Body, | |
| 67337 
4254cfd15b00
more tight HTML output: avoid extra lines within <pre>;
 wenzelm parents: 
67336diff
changeset | 432 | css: String = "isabelle.css", | 
| 
4254cfd15b00
more tight HTML output: avoid extra lines within <pre>;
 wenzelm parents: 
67336diff
changeset | 433 | hidden: Boolean = true, | 
| 75393 | 434 | structural: Boolean = true | 
| 435 |   ): String = {
 | |
| 64359 | 436 | cat_lines( | 
| 73129 | 437 | List( | 
| 438 | header, | |
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 439 | output( | 
| 66000 | 440 |           XML.elem("head", head_meta :: (if (css == "") Nil else List(style_file(css))) ::: head),
 | 
| 67337 
4254cfd15b00
more tight HTML output: avoid extra lines within <pre>;
 wenzelm parents: 
67336diff
changeset | 441 | hidden = hidden, structural = structural), | 
| 
4254cfd15b00
more tight HTML output: avoid extra lines within <pre>;
 wenzelm parents: 
67336diff
changeset | 442 |         output(XML.elem("body", body),
 | 
| 73129 | 443 | hidden = hidden, structural = structural), | 
| 444 | footer)) | |
| 65997 
e3dc9ea67a62
output control symbols like ML version, with optionally hidden source;
 wenzelm parents: 
65993diff
changeset | 445 | } | 
| 64359 | 446 | |
| 65838 | 447 | |
| 66000 | 448 | /* fonts */ | 
| 449 | ||
| 74709 | 450 |   val fonts_path: Path = Path.explode("fonts")
 | 
| 451 | ||
| 75393 | 452 |   def init_fonts(dir: Path): Unit = {
 | 
| 74709 | 453 | val fonts_dir = Isabelle_System.make_directory(dir + HTML.fonts_path) | 
| 454 | for (entry <- Isabelle_Fonts.fonts(hidden = true)) | |
| 455 | Isabelle_System.copy_file(entry.path, fonts_dir) | |
| 456 | } | |
| 457 | ||
| 458 | def fonts_dir(prefix: String)(ttf_name: String): String = prefix + "/" + ttf_name | |
| 459 | ||
| 66000 | 460 | def fonts_url(): String => String = | 
| 69374 | 461 | (for (entry <- Isabelle_Fonts.fonts(hidden = true)) | 
| 70743 
342b0a1fc86d
proper file name instead of font name (amending dc9a39c3f75d);
 wenzelm parents: 
69804diff
changeset | 462 | yield (entry.path.file_name -> Url.print_file(entry.path.file))).toMap | 
| 66000 | 463 | |
| 75393 | 464 |   def fonts_css(make_url: String => String = fonts_url()): String = {
 | 
| 69360 | 465 | def font_face(entry: Isabelle_Fonts.Entry): String = | 
| 66000 | 466 | cat_lines( | 
| 467 | List( | |
| 468 |           "@font-face {",
 | |
| 69360 | 469 | " font-family: '" + entry.family + "';", | 
| 69366 | 470 |           "  src: url('" + make_url(entry.path.file_name) + "') format('truetype');") :::
 | 
| 69360 | 471 |         (if (entry.is_bold) List("  font-weight: bold;") else Nil) :::
 | 
| 472 |         (if (entry.is_italic) List("  font-style: italic;") else Nil) :::
 | |
| 66000 | 473 |         List("}"))
 | 
| 474 | ||
| 71601 | 475 |     ("/* Isabelle fonts */" :: Isabelle_Fonts.fonts(hidden = true).map(font_face))
 | 
| 69362 | 476 |       .mkString("", "\n\n", "\n")
 | 
| 66000 | 477 | } | 
| 478 | ||
| 76618 | 479 | def fonts_css_dir(prefix: String = ""): String = | 
| 480 | fonts_css(fonts_dir(Url.append_path(prefix, fonts_path.implode))) | |
| 66000 | 481 | |
| 74709 | 482 | |
| 483 | /* document directory context (fonts + css) */ | |
| 484 | ||
| 65998 | 485 |   def isabelle_css: Path = Path.explode("~~/etc/isabelle.css")
 | 
| 486 | ||
| 75940 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 487 | def write_document(base_dir: Path, name: String, head: XML.Body, body: XML.Body, | 
| 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 488 | root: Option[Path] = None, | 
| 69366 | 489 | css: String = isabelle_css.file_name, | 
| 67337 
4254cfd15b00
more tight HTML output: avoid extra lines within <pre>;
 wenzelm parents: 
67336diff
changeset | 490 | hidden: Boolean = true, | 
| 75393 | 491 | structural: Boolean = true | 
| 492 |   ): Unit = {
 | |
| 75940 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 493 | Isabelle_System.make_directory(base_dir) | 
| 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 494 | val fonts_prefix = relative_href(root getOrElse base_dir, base = Some(base_dir)) | 
| 75938 
17d51bdabded
proper fonts_prefix (amending c14409948063): default is "" due to self-cancellation of dir;
 wenzelm parents: 
75933diff
changeset | 495 | val fonts = fonts_css_dir(fonts_prefix) | 
| 75940 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 496 | File.write(base_dir + isabelle_css.base, fonts + "\n\n" + File.read(isabelle_css)) | 
| 
c6edbc025fae
clarified signature: terminology of "base" (here) vs. "root" (there);
 wenzelm parents: 
75939diff
changeset | 497 | File.write(base_dir + Path.basic(name), | 
| 67337 
4254cfd15b00
more tight HTML output: avoid extra lines within <pre>;
 wenzelm parents: 
67336diff
changeset | 498 | output_document(head, body, css = css, hidden = hidden, structural = structural)) | 
| 65838 | 499 | } | 
| 33984 | 500 | } |