From ecc4a3ffbf4f6ae49880a7fa4a005eaeb4c4bbe2 Mon Sep 17 00:00:00 2001 From: PatrikBak Date: Fri, 8 May 2026 01:14:52 +0200 Subject: [PATCH] Render handout section and environment titles via MathRendererClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section titles and environment subtitles (theorem/exercise/problem names) were rendered as plain strings or as a fragment-per-node tree, so any inline math like \Problem{0}{zhodnosť $Ssu$} either showed visible dollars or rendered with broken baseline alignment inside the inline-flex pill. They now go through MathRendererClient with a reconstructed source string, matching how the document title and subtitle already render. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../features/handouts/HandoutDetail.tsx | 40 ++++++------------- web/src/components/math/ContentRenderer.tsx | 2 +- web/src/components/math/utils/math-render.ts | 35 ++++++++++++++++ 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/web/src/components/features/handouts/HandoutDetail.tsx b/web/src/components/features/handouts/HandoutDetail.tsx index e78240a0..1a27cacb 100644 --- a/web/src/components/features/handouts/HandoutDetail.tsx +++ b/web/src/components/features/handouts/HandoutDetail.tsx @@ -9,12 +9,9 @@ import type { RawContentBlock, } from '@/components/features/handouts/handout-content-types' import type { SectionMetadata } from '@/components/features/handouts/handout-utils' -import { - renderBlocks, - renderInlineContent, - renderRawContentBlock, -} from '@/components/math/ContentRenderer' +import { renderBlocks, renderRawContentBlock } from '@/components/math/ContentRenderer' import { MathRendererClient } from '@/components/math/MathRendererClient' +import { inlineBlockToMathSource } from '@/components/math/utils/math-render' import { AppLink } from '@/components/shared/components/AppLink' import { ArticleSection } from '@/components/shared/components/ArticleSection' import { HelpTooltip } from '@/components/shared/components/HelpTooltip' @@ -71,33 +68,22 @@ const imageType = 'handouts' /** * Renders the optional title of an environment block (e.g. theorem name, - * definition concept) into a React node. + * definition concept) as a single math-aware string, matching how the + * document title and subtitle are rendered. * * @param title The optional inline title block, or null/undefined if absent. - * @param imagesById Lookup map of {@link HandoutImage}s keyed by content ID. - * @param imageMissingText Fallback text to display when an image is not found. * @returns The rendered React node, or null if no title was provided. */ -function renderTitle( - title: RawContentBlock | null | undefined, - imagesById: Record, - imageMissingText: string -): React.ReactNode { +function renderTitle(title: RawContentBlock | null | undefined): React.ReactNode { // No title if (!title) return null - // Plain text - if (title.type === 'text') { - return title.text - } - - // For complex titles, render as React elements to preserve formatting. - if (title.type === 'paragraph' || title.type === 'bold' || title.type === 'italic') { - return renderInlineContent(title.content, imagesById, imageType, imageMissingText) - } + // Reconstruct the raw source string so KaTeX sees the whole title at once + const rawString = inlineBlockToMathSource(title) + if (!rawString) return null - // Fallback for unexpected types, though paragraph should cover most cases. - return renderRawContentBlock(title, imagesById, imageType, imageMissingText) + // Single MathRendererClient pass keeps text and math on the same baseline + return } /** @@ -134,7 +120,7 @@ function renderDocumentSections( imagesById: Record, t: HandoutsTranslator, imageMissingText: string -) { +): React.ReactNode { // Translate the environment labels const localizedEnvironmentLabelByType: Record = { theorem: t('environments.theorem'), @@ -175,7 +161,7 @@ function renderDocumentSections( id={metadata.id} number={metadata.label} title={section.title} - titleContent={section.title} + titleContent={} > {section.text.content.map((contentBlock, contentBlockIndex) => { if ( @@ -196,7 +182,7 @@ function renderDocumentSections( const environmentBaseTitle = localizedEnvironmentLabelByType[contentBlock.type] // The optional inline name authored in TeX (e.g. \Definition{Aritmetický průměr}). - const userProvidedTitle = renderTitle(contentBlock.title, imagesById, imageMissingText) + const userProvidedTitle = renderTitle(contentBlock.title) // Difficulty asterisks are problem-only (e.g. "Úloha 4**"). const difficultyStars = diff --git a/web/src/components/math/ContentRenderer.tsx b/web/src/components/math/ContentRenderer.tsx index 3c3025ca..3e4c9f20 100644 --- a/web/src/components/math/ContentRenderer.tsx +++ b/web/src/components/math/ContentRenderer.tsx @@ -328,7 +328,7 @@ export function renderRawContentBlock( * no block-level elements like

are created. This is suitable for rendering * content inside elements that expect inline content, such as badges or titles. */ -export function renderInlineContent( +function renderInlineContent( content: RawContentBlock[], imagesById: Record, imageType: ImageType, diff --git a/web/src/components/math/utils/math-render.ts b/web/src/components/math/utils/math-render.ts index 7ba0005f..ea1561da 100644 --- a/web/src/components/math/utils/math-render.ts +++ b/web/src/components/math/utils/math-render.ts @@ -1,5 +1,40 @@ import katex from 'katex' +import type { RawContentBlock } from '@/components/features/handouts/handout-content-types' + +/** + * Flattens a parsed inline content block back to its raw source string, wrapping + * math nodes in `$...$` / `$$...$$` and recursing through paragraph/bold/italic + * containers. The result can be fed straight into {@link renderMathContentToHtml} + * (or a `MathRendererClient`) so text and math render on the same baseline as a + * single KaTeX-aware string. + * + * @param block The inline content block to flatten, or null/undefined if absent. + * + * @returns The reconstructed source string, or `''` if the block is absent or unsupported. + */ +export function inlineBlockToMathSource(block: RawContentBlock | null | undefined): string { + // No block + if (!block) return '' + + switch (block.type) { + // Plain text passes through verbatim + case 'text': + return block.text + // Math is wrapped back in delimiters matching its display mode + case 'math': + return block.isDisplay ? `$$${block.text}$$` : `$${block.text}$` + // Container blocks recurse into their children + case 'paragraph': + case 'bold': + case 'italic': + return block.content.map(inlineBlockToMathSource).join('') + // Other block types (links, lists, images, ...) are not expected in inline titles + default: + return '' + } +} + /** * Renders a string containing inline ($...$) and display ($$...$$) LaTeX to HTML using KaTeX. *