Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 13 additions & 27 deletions web/src/components/features/handouts/HandoutDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<string, HandoutImage>,
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 <MathRendererClient content={rawString} />
}

/**
Expand Down Expand Up @@ -134,7 +120,7 @@ function renderDocumentSections(
imagesById: Record<string, HandoutImage>,
t: HandoutsTranslator,
imageMissingText: string
) {
): React.ReactNode {
// Translate the environment labels
const localizedEnvironmentLabelByType: Record<HandoutEnvironmentType, string> = {
theorem: t('environments.theorem'),
Expand Down Expand Up @@ -175,7 +161,7 @@ function renderDocumentSections(
id={metadata.id}
number={metadata.label}
title={section.title}
titleContent={section.title}
titleContent={<MathRendererClient content={section.title} />}
>
{section.text.content.map((contentBlock, contentBlockIndex) => {
if (
Expand All @@ -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 =
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/math/ContentRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ export function renderRawContentBlock(
* no block-level elements like <p> 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<string, ProblemImage>,
imageType: ImageType,
Expand Down
35 changes: 35 additions & 0 deletions web/src/components/math/utils/math-render.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down
Loading