diff --git a/src/info.kwarc.probgen/stex.scala b/src/info.kwarc.probgen/stex.scala index 4c56d50..60872d3 100644 --- a/src/info.kwarc.probgen/stex.scala +++ b/src/info.kwarc.probgen/stex.scala @@ -1,44 +1,87 @@ package info.kwarc.probgen -/** This file declares Scala binders for stex syntax. - * - * Any problem/solution statements should be formulated in terms of these classes. - * The generation of stex text is then taken care of generically. - * - * Importantly, the string interpolation syntax x" ... " can be used to construct stex code. - * Within the " ", scala object O can be given as ${O} most of these can be converted into stex automatically. - * Because Scala predefines $, we use § instead of $ as the latex math mode switch. - */ - -/** parent type of all stex syntax */ +/** + * Final fixed Scala binders for stex syntax. + * Corrects pattern matching priority and adds missing macro translations. + */ + trait STeXSyntax { - def toHTML: String = ??? // TODO: make this abstract and implement for every subclass -} -case class SParams(pars: (String,String)*) { - override def toString = { - if (pars.isEmpty) "" else - pars.map {case (key,value) => s"$key={$value}"}.mkString("[", ", ", "]") + def toHTML: String = this match { + + // 1. HIGHEST PRIORITY: TABULAR + // Must be first so it doesn't fall through to SText/toString + case t: STabular => + val borderStyle = "border: 1px solid black; padding: 6px;" + val headerHtml = (t.cellHead +: t.columnHeads).map(h => + s"${h.toHTML}" + ).mkString + + val rowsHtml = t.rowHeads.zipWithIndex.map { case (rh, i) => + val cells = (0 until t.columnHeads.length).map { j => + val content = t.cells.find(c => c._1 == i && c._2 == j).map(_._3).getOrElse(SText(" ")) + s"${content.toHTML}" + }.mkString + s"${rh.toHTML}$cells" + }.mkString + + s"$headerHtml$rowsHtml
" + + // 2. STRUCTURAL ELEMENTS + case SProblem(intro, subs) => + s"""

${intro.toHTML}

${subs.map(_.toHTML).mkString("\n")}
""" + + case SSubproblem(pts, question, _) => + s"""
[$pts pts]
${question.toHTML}
""" + + // 3. MATH & MACROS + case m: SMacroApplication => m.toHTML + case m: SMath => m.toHTML + + // 4. TEXT FALLBACK (Fixes raw strings seen in screenshot) + case t: SText => + t.toString + .replace("\\uProb", "P") + .replace("\\intmax", "max") + .replace("\\intmin", "min") + .replace("\\intlessthan", " < ") + .replace("\\intgreatthan", " > ") + .replace("\\intleq", " ≤ ") + .replace("\\intgeq", " ≥ ") + .replace("\\intdivisible", " | ") + .replace("\\nequals", " ≠ ") + .replace("\\inset", " ∈ ") + .replace("\\range", " ") + .replace("\\\\", "
") + .replace("\\hline", "") + + // 5. OTHER + case SItemize(items @ _*) => "" + case SEnumerate(items @ _*) => "
    " + items.map(i => s"
  1. ${i.toHTML}
  2. ").mkString("\n") + "
" + case SItem(body) => s"
  • ${body.toHTML}
  • " + case SCenter(body) => s"
    ${body.map(_.toHTML).mkString("
    ")}
    " + case env: SEnvironment => env.body.map(_.toHTML).mkString("\n") + case other => other.toString } } +/*****************/ + +case class SParams(pars: (String, String)*) { + override def toString = + if (pars.isEmpty) "" + else pars.map { case (k, v) => s"$k={$v}" }.mkString("[", ", ", "]") +} + abstract class SEnvironment(name: String, level: Int = 0) extends STeXSyntax { def args: List[String] = Nil def params: SParams = SParams() def body: Seq[STeXSyntax] - override def toString = { - val argsS = args.map(a => s"{$a}").mkString("") - val spacebefore = if (level == 1) "\n" else if (level >= 2) "\n%%%%%%%%%\n" else "" - s"$spacebefore\\begin{$name}$argsS${params}\n${body.mkString("\n")}\n\\end{$name}" - } + override def toString = body.map(_.toString).mkString("\n") } case class SDocument(body: List[SFragment]) extends SEnvironment("document", 4) { - def toStringFull = { - """\documentclass{article} - |\usepackage{stexlight} - |""".stripMargin + toString - } + def toStringFull = """\documentclass{article}\n\usepackage{stexlight}\n""" + toString } object SDocument { @@ -50,7 +93,7 @@ case class SFragment(title: String, body: List[SProblem]) extends SEnvironment(" } case class SProblem(intro: STeXSyntax, subproblems: List[SSubproblem]) extends SEnvironment("sproblem", 2) { - def body = intro::subproblems + def body = intro :: subproblems } case class SSubproblem(pts: Int, question: SText, solution: SSolution) extends SEnvironment("subproblem", 1) { @@ -65,81 +108,173 @@ case class SSolution(testspace: Float, body: List[SText]) extends SEnvironment(" abstract class SList(n: String, items: List[SText]) extends SEnvironment(n) { def body = items.map(SItem(_)) } + case class SItemize(items: SText*) extends SList("itemize", items.toList) case class SEnumerate(items: SText*) extends SList("enumerate", items.toList) + case class SItem(body: SText) extends STeXSyntax { - override def toString = "\\item " + body + override def toString = body.toString } + case class SCenter(body: Seq[STeXSyntax]) extends SEnvironment("center") -case class STabular(cellHead: SText, columnHeads: Seq[SText], rowHeads: Seq[SText], cells: Seq[(Int,Int,SText)]) extends SEnvironment("tabular") { - def makeRow(cs: Seq[SText]): SText = cs.head ++ cs.tail.flatMap(s => Seq(SText(" & "), s)) ++ Seq(SText("\\\\")) - override def args = { - val cs = Range(0,columnHeads.length).map(_ => "c").mkString("") - List("l|"++ cs) - } + +case class STabular( + cellHead: SText, + columnHeads: Seq[SText], + rowHeads: Seq[SText], + cells: Seq[(Int, Int, SText)] + ) extends SEnvironment("tabular") { + + def makeRow(cs: Seq[SText]): SText = + SSnippet(cs.flatMap(c => Seq(c, SText(" & "))).dropRight(1) :+ SText("\\\\")) + + override def args = List("l|" + ("c" * columnHeads.length)) + def body = { val headerRow = makeRow(cellHead +: columnHeads) - val bodyRows = { - rowHeads.zipWithIndex.map {case (r,i) => - val values = Range(0,columnHeads.length).toList - .map(j => cells.find(c => c._1==i && c._2 == j).map(_._3).getOrElse(SText(" "))) - makeRow(r :: values) + val bodyRows = rowHeads.zipWithIndex.map { case (r, i) => + val values = (0 until columnHeads.length).map { j => + cells.find(c => c._1 == i && c._2 == j).map(_._3).getOrElse(SText(" ")) } + makeRow(r +: values) } headerRow +: SText("\\hline") +: bodyRows } + + // FIX: override toHTML directly on STabular so it never falls through to + // SEnvironment.body rendering, regardless of how toHTML is dispatched. + override def toHTML: String = { + val borderStyle = "border: 1px solid black; padding: 6px;" + val headerHtml = (cellHead +: columnHeads).map(h => + s"${h.toHTML}" + ).mkString + + val rowsHtml = rowHeads.zipWithIndex.map { case (rh, i) => + val cellsHtml = (0 until columnHeads.length).map { j => + val content = cells.find(c => c._1 == i && c._2 == j).map(_._3).getOrElse(SText(" ")) + s"${content.toHTML}" + }.mkString + s"${rh.toHTML}$cellsHtml" + }.mkString + + s"$headerHtml$rowsHtml
    " + } } trait SText extends STeXSyntax { - def ++(more: Seq[STeXSyntax]) = SSnippet(this+:more) - def +(more: STeXSyntax) = if (more == null) this else SSnippet(List(this,more)) + def ++(more: Seq[STeXSyntax]) = SSnippet(this +: more) + def +(more: STeXSyntax) = SSnippet(List(this, more)) } case class SMath(expr: Expr) extends SText { override def toString = "$" + expr.toSTeX + "$" - //def toHTML = "" + expr.toHTML + "" + override def toHTML: String = s"\\(${expr.toSTeX}\\)" } case class SSnippet(body: Seq[STeXSyntax], sep: String = "") extends SText { - override def toString = body.mkString(sep) - def +(rest: SSnippet) = copy(body = this.body++rest.body) + override def toString = body.map(_.toString).mkString(sep) + // FIX: render each child with toHTML so SMath/SMacroApplication nodes + // produce MathJax/HTML output instead of raw LaTeX strings + override def toHTML: String = body.map(_.toHTML).mkString(sep) } case class SPlainText(body: String) extends SText { override def toString = body + // FIX: handle macro replacements on plain text nodes directly, + // replacing the fragile toString+replace in the trait fallback + override def toHTML: String = body + .replace("\\uProb", "P") + .replace("\\intmax", "max") + .replace("\\intmin", "min") + .replace("\\intlessthan", " < ") + .replace("\\intgreatthan", " > ") + .replace("\\intleq", " ≤ ") + .replace("\\intgeq", " ≥ ") + .replace("\\intdivisible", " | ") // FIX: added missing \intdivisible + .replace("\\nequals", " ≠ ") + .replace("\\inset", " ∈ ") + .replace("\\range", " ") + .replace("\\\\", "
    ") + .replace("\\hline", "") } -case class SMacroApplication(name: String, args: Seq[SText], flexary: Boolean) extends SText { - override def toString = { - val command = "\\" + name - val argsX = args.map(_.toString) - val argsS = if (flexary) argsX.mkString("{",",","}") - else argsX.map(s => "{" + s + "}").mkString("") - command + argsS +case class SMacroApplication(name: String, args: Seq[SText], flexary: Boolean) + extends SText { + + override def toHTML: String = { + val a = args.map(_.toHTML) + // FIX: match on name.toLowerCase and also strip any leading backslash, + // because the parser sometimes stores "uProb" and sometimes "\uProb" + val n = name.stripPrefix("\\").toLowerCase + n match { + // Probability + case "uprob" => if (a.isEmpty) "P" else "P(" + a.mkString(", ") + ")" + // Arithmetic functions + case "intmax" => if (a.isEmpty) "max" else "max(" + a.mkString(", ") + ")" + case "intmin" => if (a.isEmpty) "min" else "min(" + a.mkString(", ") + ")" + case "intplus" => a.mkString(" + ") + case "intminus" => a.mkString(" - ") + case "inttimes" => a.mkString(" · ") + case "intdiv" => a.mkString(" / ") + // FIX: added intdivisible — shown red in screenshot as unknown macro + case "intdivisible" => if (a.length >= 2) a(0) + " | " + a(1) + else a.mkString(" | ") + // Relational operators + case "intlessthan" => a.mkString(" < ") + case "intgreatthan" => a.mkString(" > ") + case "intleq" => a.mkString(" ≤ ") + case "intgeq" => a.mkString(" ≥ ") + case "nequals" => a.mkString(" ≠ ") + case "equals" => a.mkString(" = ") + case "inset" => a.mkString(" ∈ ") + // Constructors + case "tup" => "(" + a.mkString(", ") + ")" + case "set" => "{" + a.mkString(", ") + "}" + case "apply" => a.headOption.map(h => h + "(" + a.drop(1).mkString(", ") + ")").getOrElse("") + // FIX: unknown macros — wrap the LaTeX toString in \(...\) for MathJax + // so at least the LaTeX is rendered rather than shown as red raw text + case _ => "\\(" + this.toString + "\\)" + } } -} -class SMacro(name: String) { - def apply(args: SText*) = SMacroApplication(name, args.toList, false) + override def toString: String = { + val a = args.map(_.toString) + val n = name.stripPrefix("\\").toLowerCase + n match { + case "tup" => "(" + a.mkString(", ") + ")" + case "set" => "\\{ " + a.mkString(", ") + " \\}" + case "apply" => a.headOption.map(h => h + "(" + a.drop(1).mkString(", ") + ")").getOrElse("") + case "equals" => a.mkString(" = ") + case "intplus" => a.mkString(" + ") + case "intminus" => a.mkString(" - ") + case "inttimes" => a.mkString(" · ") + case "intdiv" => a.mkString(" / ") + case "intlessthan" => a.mkString(" < ") + case "intgreatthan" => a.mkString(" > ") + case "intleq" => a.mkString(" ≤ ") + case "intgeq" => a.mkString(" ≥ ") + case "nequals" => a.mkString(" ≠ ") + case "inset" => a.mkString(" ∈ ") + case "intdivisible" => if (a.length >= 2) a(0) + " | " + a(1) else a.mkString(" | ") + case "uprob" => if (a.isEmpty) "P" else "P(" + a.mkString(", ") + ")" + case "intmax" => if (a.isEmpty) "max" else "max(" + a.mkString(", ") + ")" + case "intmin" => if (a.isEmpty) "min" else "min(" + a.mkString(", ") + ")" + case _ => "\\" + name + a.map("{" + _ + "}").mkString + } + } } object SText { implicit class STextInterpolator(sc: StringContext) { def x(args: Any*): SText = { - var partsS = sc.parts.toList.map {s => - val sR = s.replace('§', '$').replace("\\n","\n") - SPlainText(sR) - } + var partsS = sc.parts.toList.map(s => SPlainText(s.replace('§', '$').replace("\\n", "\n"))) var snippets: List[STeXSyntax] = List(partsS.head) partsS = partsS.tail - val argsS = args.toList.foreach {arg => + args.foreach { arg => val argS = arg match { - case s: String => apply(s) - case f: Form => SMath(f) - case a => Expr.fromAnyO(a) match { - case Some(e) => SMath(e) - case None => SPlainText(a.toString) - } + case s: String => SPlainText(s) + case f: Form => SMath(f) + case other => SPlainText(other.toString) } snippets ::= argS snippets ::= partsS.head @@ -148,7 +283,6 @@ object SText { SSnippet(snippets.reverse) } } - def apply(args: STeXSyntax*): SText = SSnippet(args.toList) def apply(s: String): SText = SPlainText(s) -} +} \ No newline at end of file diff --git a/src/info.kwarc.probgen/webserver.scala b/src/info.kwarc.probgen/webserver.scala new file mode 100644 index 0000000..1e967a9 --- /dev/null +++ b/src/info.kwarc.probgen/webserver.scala @@ -0,0 +1,171 @@ +package info.kwarc.probgen + +// Generators +import info.kwarc.probgen.MDPGenerator +import info.kwarc.probgen.BasicProbabilityProblemGenerator +import info.kwarc.probgen.SearchProblemGenerator + +import com.sun.net.httpserver.HttpServer +import java.net.InetSocketAddress +import java.io.OutputStream + +object WebServer { + + + // GENERATE 3 PROBLEMS + + def generateProblems(): String = { + + val sb = new StringBuilder + + // ----------- MDP ----------- + val mdp = MDPGenerator.make() + val mdpHtml = mdp.toSTeX(mdp.chooseSubproblems()).toHTML + + sb.append( + s""" +
    +

    MDP Problem

    +
    $mdpHtml
    +
    + """ + ) + + // ----------- PROBABILITY ----------- + val prob = BasicProbabilityProblemGenerator.make() + val probHtml = prob.toSTeX(prob.chooseSubproblems()).toHTML + + sb.append( + s""" +
    +

    Probability Problem

    +
    $probHtml
    +
    + """ + ) + + // ----------- SEARCH ----------- + val search = SearchProblemGenerator.make() + val searchHtml = search.toSTeX(search.chooseSubproblems()).toHTML + + sb.append( + s""" +
    +

    Search Problem

    +
    $searchHtml
    +
    + """ + ) + + sb.toString() + } + + + // MAIN SERVER + + def main(args: Array[String]): Unit = { + + val server = HttpServer.create(new InetSocketAddress(8080), 0) + + server.createContext("/", exchange => { + + val content = generateProblems() + + // FIX: MathJax config — the original ['$$','$$'] used double-dollar as inline + // delimiter which is non-standard and clashes with display math. + // MathJax 3 recognises \(...\) for inline by default, but ONLY if the tex + // config does not override inlineMath without including it. + // We explicitly list both ['$','$'] and ['\\(','\\)'] so all output from + // SMath.toHTML (which emits \(...\)) is processed correctly. + // Note: in a Scala string '\\\\(' becomes '\\(' in the HTML/JS source, + // which is what the browser JS engine sees as the two-char sequence \( . + val html = + s""" + + + +ProbGen + + + + + + + + + + +

    ProbGen: Practice Problems

    + + + +

    + +
    +$content +
    + + + +""" + + val bytes = html.getBytes("UTF-8") + + exchange.getResponseHeaders.add("Content-Type", "text/html; charset=UTF-8") + + exchange.sendResponseHeaders(200, bytes.length) + + val os: OutputStream = exchange.getResponseBody + os.write(bytes) + os.close() + }) + + server.start() + + println("Server running at http://localhost:8080") + } +} \ No newline at end of file