-
+
{expanded && (
-
+
{record.signature}
- {record.value != null && `\n:=\n${record.value}`}
+ {record.value
+ ? `\n${record.value.trimStart().startsWith(":=") ? record.value : `:= ${record.value}`}`
+ : ""}
)}
diff --git a/frontend/src/components/LatexText.tsx b/frontend/src/components/LatexText.tsx
index bdc7502..0648305 100644
--- a/frontend/src/components/LatexText.tsx
+++ b/frontend/src/components/LatexText.tsx
@@ -14,8 +14,8 @@ type Part =
function parseParts(text: string): Part[] {
const parts: Part[] = [];
- // Match $$...$$ first (display), then $...$ (inline)
- const re = /(\$\$[\s\S]*?\$\$|\$[^$\n]+?\$)/g;
+ // Match $$...$$, \[...\] (display), then \(...\), $...$ (inline)
+ const re = /(\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\)|\$[^$\n]+?\$)/g;
let last = 0;
let match: RegExpExecArray | null;
@@ -26,6 +26,10 @@ function parseParts(text: string): Part[] {
const raw = match[0];
if (raw.startsWith("$$")) {
parts.push({ type: "block", latex: raw.slice(2, -2) });
+ } else if (raw.startsWith("\\[")) {
+ parts.push({ type: "block", latex: raw.slice(2, -2) });
+ } else if (raw.startsWith("\\(")) {
+ parts.push({ type: "inline", latex: raw.slice(2, -2) });
} else {
parts.push({ type: "inline", latex: raw.slice(1, -1) });
}
@@ -38,15 +42,46 @@ function parseParts(text: string): Part[] {
return parts;
}
+function escapeHtml(s: string): string {
+ return s
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+const MATHBB_UNICODE: Record
= {
+ R: "ℝ", C: "ℂ", Z: "ℤ", N: "ℕ", Q: "ℚ",
+ P: "ℙ", H: "ℍ", F: "𝔽", K: "𝕂",
+};
+
+// The informal-description generator sometimes emits math-mode commands like
+// `\mathbb{R}` inside `\text{...}`, which KaTeX (correctly) refuses to render.
+// Replace those with their Unicode equivalents so the text renders cleanly.
+function sanitizeLatex(latex: string): string {
+ const textGroup = /\\text\{((?:[^{}]|\{[^{}]*\})*)\}/g;
+ return latex.replace(textGroup, (_match, inner: string) => {
+ const fixed = inner.replace(
+ /\\mathbb\{([A-Z])\}/g,
+ (m, letter: string) => MATHBB_UNICODE[letter] ?? m
+ );
+ return `\\text{${fixed}}`;
+ });
+}
+
function renderLatex(latex: string, displayMode: boolean): string {
+ const source = sanitizeLatex(latex);
try {
- return katex.renderToString(latex, {
+ return katex.renderToString(source, {
displayMode,
- throwOnError: false,
+ throwOnError: true,
+ strict: "ignore",
trust: false,
});
} catch {
- return latex;
+ const [open, close] = displayMode ? ["\\[", "\\]"] : ["$", "$"];
+ return escapeHtml(`${open}${latex}${close}`);
}
}
diff --git a/frontend/src/components/MobileKindFilter.tsx b/frontend/src/components/MobileKindFilter.tsx
new file mode 100644
index 0000000..932c420
--- /dev/null
+++ b/frontend/src/components/MobileKindFilter.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import type { DeclarationKind } from "@/types";
+
+const ALL_KINDS: DeclarationKind[] = [
+ "theorem",
+ "definition",
+ "structure",
+ "inductive",
+ "classInductive",
+ "instance",
+ "abbrev",
+ "axiom",
+ "opaque",
+ "proofWanted",
+];
+
+const KIND_LABELS: Partial> = {
+ theorem: "Theorem",
+ definition: "Definition",
+ structure: "Structure",
+ inductive: "Inductive",
+ classInductive: "Class",
+ instance: "Instance",
+ abbrev: "Abbrev",
+ axiom: "Axiom",
+ opaque: "Opaque",
+ proofWanted: "Proof Wanted",
+};
+
+interface Props {
+ pathname: string;
+ currentKinds: DeclarationKind[];
+}
+
+export default function MobileKindFilter({ pathname, currentKinds }: Props) {
+ const router = useRouter();
+
+ function toggleKind(kind: DeclarationKind) {
+ const isVisible = currentKinds.length === 0 || currentKinds.includes(kind);
+ let newKinds: DeclarationKind[];
+ if (isVisible) {
+ newKinds =
+ currentKinds.length === 0
+ ? ALL_KINDS.filter((k) => k !== kind)
+ : currentKinds.filter((k) => k !== kind);
+ } else {
+ newKinds = [...currentKinds, kind];
+ }
+ const isAll = newKinds.length === 0 || newKinds.length === ALL_KINDS.length;
+ router.replace(isAll ? pathname : `${pathname}?kinds=${newKinds.join(",")}`);
+ }
+
+ return (
+
+ {currentKinds.length > 0 && (
+
+ )}
+ {ALL_KINDS.map((kind) => {
+ const isVisible = currentKinds.length === 0 || currentKinds.includes(kind);
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 4ab5739..584d665 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -33,4 +33,5 @@ export interface QueryResult {
export interface ModuleInfo {
name: LeanName;
count: number;
+ docstring: string | null;
}
diff --git a/server.py b/server.py
index e1a24ca..23a5220 100644
--- a/server.py
+++ b/server.py
@@ -119,6 +119,7 @@ class UserFeedback(BaseModel):
class ModuleInfo(BaseModel):
name: LeanName
count: int
+ docstring: str | None = None
@app.get("/modules")
@@ -127,11 +128,12 @@ def list_modules(request: Request) -> list[ModuleInfo]:
with app.pool.connection() as conn:
with conn.cursor(row_factory=class_row(ModuleInfo)) as cursor:
cursor.execute("""
- SELECT d.module_name AS name, COUNT(*) AS count
+ SELECT d.module_name AS name, COUNT(*) AS count, mo.docstring
FROM declaration d
+ INNER JOIN module mo ON mo.name = d.module_name
WHERE d.visible = TRUE
AND EXISTS(SELECT 1 FROM informal i WHERE i.symbol_name = d.name)
- GROUP BY d.module_name
+ GROUP BY d.module_name, mo.docstring
ORDER BY d.module_name
""")
return cursor.fetchall()