feat: implement LaTeX support with KaTeX and python-markdown-math
3 - Integration of KaTeX in frontend (SvelteKit) with a custom renderMath` action. - Added `python-markdown-math` extension in backend (Flask). to handle math blocks. - Configured Vite to handle KaTeX during SSR and browser rendering. - Added a LaTeX test lesson and updated home navigation. - Supported both inline ($) and block ($$) math rendering, including multi-line support.
This commit is contained in:
parent
dce4916b94
commit
b8fb67115e
|
|
@ -12,6 +12,7 @@
|
|||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@types/katex": "^0.16.7",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
|
|
@ -23,6 +24,7 @@
|
|||
"@codemirror/theme-one-dark": "^6.1.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"highlight.js": "^11.11.0",
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^15.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
|
||||
<meta name="theme-color" content="#0d6efd" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { browser } from '$app/environment';
|
||||
import katex from 'katex';
|
||||
|
||||
export async function autoRenderMath(node: HTMLElement) {
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
const renderMathInElement = (await import('katex/dist/contrib/auto-render')).default;
|
||||
await import('katex/dist/katex.min.css');
|
||||
|
||||
// 1. First, handle the standard delimiters
|
||||
renderMathInElement(node, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '$', right: '$', display: false },
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true }
|
||||
],
|
||||
throwOnError: false
|
||||
});
|
||||
|
||||
// 2. Then, handle <script type="math/tex"> tags generated by python-markdown-math
|
||||
const mathScripts = node.querySelectorAll('script[type^="math/tex"]');
|
||||
mathScripts.forEach((script) => {
|
||||
const displayMode = script.getAttribute('type') === 'math/tex; mode=display';
|
||||
const content = script.textContent || '';
|
||||
const span = document.createElement('span');
|
||||
|
||||
try {
|
||||
katex.render(content, span, {
|
||||
displayMode: displayMode,
|
||||
throwOnError: false
|
||||
});
|
||||
script.parentNode?.replaceChild(span, script);
|
||||
} catch (err) {
|
||||
console.error('KaTeX rendering error:', err);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('KaTeX failed to load:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Svelte action to render LaTeX math in an element using KaTeX auto-render.
|
||||
*/
|
||||
export function renderMath(node: HTMLElement) {
|
||||
if (browser) {
|
||||
autoRenderMath(node);
|
||||
}
|
||||
|
||||
return {
|
||||
update() {
|
||||
if (browser) {
|
||||
autoRenderMath(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import LessonCard from '$components/LessonCard.svelte';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { renderMath } from '$lib/actions/renderMath';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
|
@ -10,7 +11,7 @@
|
|||
</svelte:head>
|
||||
|
||||
{#if data.homeContent}
|
||||
<section class="home-intro">
|
||||
<section class="home-intro" use:renderMath>
|
||||
{@html data.homeContent}
|
||||
</section>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
import { createFloatingPanel } from '$actions/floatingPanel.svelte';
|
||||
import { highlightAllCode } from '$actions/highlightCode';
|
||||
import { renderCircuitEmbeds } from '$actions/renderCircuitEmbeds';
|
||||
import { renderMath, autoRenderMath } from '$lib/actions/renderMath';
|
||||
import { tick, untrack } from 'svelte';
|
||||
import type { LessonContent } from '$types/lesson';
|
||||
|
||||
|
|
@ -279,10 +280,12 @@
|
|||
if (contentEl) {
|
||||
highlightAllCode(contentEl);
|
||||
renderCircuitEmbeds(contentEl);
|
||||
autoRenderMath(contentEl);
|
||||
}
|
||||
if (tabsEl) {
|
||||
highlightAllCode(tabsEl);
|
||||
renderCircuitEmbeds(tabsEl);
|
||||
autoRenderMath(tabsEl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -821,7 +824,7 @@
|
|||
<div class="lesson-layout" class:single-col={float.floating || isMobile}>
|
||||
<!-- Left: Lesson content (selection & copy prevention) -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="lesson-content" bind:this={contentEl} use:noSelect
|
||||
<div class="lesson-content" bind:this={contentEl} use:noSelect use:renderMath
|
||||
role="region" aria-label="Konten pelajaran"
|
||||
class:full-width={float.floating || isMobile}
|
||||
onselectstart={(e) => e.preventDefault()}
|
||||
|
|
@ -869,7 +872,7 @@
|
|||
/>
|
||||
|
||||
<!-- Editor body -->
|
||||
<div class="editor-body" bind:this={tabsEl}>
|
||||
<div class="editor-body" bind:this={tabsEl} use:renderMath>
|
||||
|
||||
<!-- Info tab panel -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { defineConfig } from 'vite';
|
|||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
ssr: {
|
||||
noExternal: ['katex']
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ gunicorn==21.2.0
|
|||
flask-cors==4.0.0
|
||||
python-dotenv==1.0.0
|
||||
requests==2.31.0
|
||||
flask-limiter==3.5.0
|
||||
flask-limiter==3.5.0
|
||||
python-markdown-math==0.8
|
||||
|
|
@ -195,7 +195,7 @@ def get_ordered_lessons_with_learning_objectives(progress=None):
|
|||
# Markdown rendering
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MD_EXTENSIONS = ['fenced_code', 'tables', 'nl2br', 'toc']
|
||||
MD_EXTENSIONS = ['fenced_code', 'tables', 'nl2br', 'toc', 'mdx_math']
|
||||
|
||||
|
||||
def _process_circuit_embeds(text):
|
||||
|
|
@ -398,4 +398,4 @@ def render_home_content():
|
|||
|
||||
parts = home_content.split('---Available_Lessons---')
|
||||
main_content = parts[0] if parts else home_content
|
||||
return md.markdown(main_content, extensions=['fenced_code', 'tables'])
|
||||
return md.markdown(main_content, extensions=['fenced_code', 'tables', 'mdx_math'])
|
||||
|
|
|
|||
Loading…
Reference in New Issue