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/adapter-node": "^5.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
|
|
@ -23,6 +24,7 @@
|
||||||
"@codemirror/theme-one-dark": "^6.1.0",
|
"@codemirror/theme-one-dark": "^6.1.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"highlight.js": "^11.11.0",
|
"highlight.js": "^11.11.0",
|
||||||
|
"katex": "^0.16.9",
|
||||||
"marked": "^15.0.0"
|
"marked": "^15.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
|
||||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
|
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
|
||||||
<meta name="theme-color" content="#0d6efd" />
|
<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%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<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">
|
<script lang="ts">
|
||||||
import LessonCard from '$components/LessonCard.svelte';
|
import LessonCard from '$components/LessonCard.svelte';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
import { renderMath } from '$lib/actions/renderMath';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if data.homeContent}
|
{#if data.homeContent}
|
||||||
<section class="home-intro">
|
<section class="home-intro" use:renderMath>
|
||||||
{@html data.homeContent}
|
{@html data.homeContent}
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
import { createFloatingPanel } from '$actions/floatingPanel.svelte';
|
import { createFloatingPanel } from '$actions/floatingPanel.svelte';
|
||||||
import { highlightAllCode } from '$actions/highlightCode';
|
import { highlightAllCode } from '$actions/highlightCode';
|
||||||
import { renderCircuitEmbeds } from '$actions/renderCircuitEmbeds';
|
import { renderCircuitEmbeds } from '$actions/renderCircuitEmbeds';
|
||||||
|
import { renderMath, autoRenderMath } from '$lib/actions/renderMath';
|
||||||
import { tick, untrack } from 'svelte';
|
import { tick, untrack } from 'svelte';
|
||||||
import type { LessonContent } from '$types/lesson';
|
import type { LessonContent } from '$types/lesson';
|
||||||
|
|
||||||
|
|
@ -279,10 +280,12 @@
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
highlightAllCode(contentEl);
|
highlightAllCode(contentEl);
|
||||||
renderCircuitEmbeds(contentEl);
|
renderCircuitEmbeds(contentEl);
|
||||||
|
autoRenderMath(contentEl);
|
||||||
}
|
}
|
||||||
if (tabsEl) {
|
if (tabsEl) {
|
||||||
highlightAllCode(tabsEl);
|
highlightAllCode(tabsEl);
|
||||||
renderCircuitEmbeds(tabsEl);
|
renderCircuitEmbeds(tabsEl);
|
||||||
|
autoRenderMath(tabsEl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -821,7 +824,7 @@
|
||||||
<div class="lesson-layout" class:single-col={float.floating || isMobile}>
|
<div class="lesson-layout" class:single-col={float.floating || isMobile}>
|
||||||
<!-- Left: Lesson content (selection & copy prevention) -->
|
<!-- Left: Lesson content (selection & copy prevention) -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- 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"
|
role="region" aria-label="Konten pelajaran"
|
||||||
class:full-width={float.floating || isMobile}
|
class:full-width={float.floating || isMobile}
|
||||||
onselectstart={(e) => e.preventDefault()}
|
onselectstart={(e) => e.preventDefault()}
|
||||||
|
|
@ -869,7 +872,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Editor body -->
|
<!-- Editor body -->
|
||||||
<div class="editor-body" bind:this={tabsEl}>
|
<div class="editor-body" bind:this={tabsEl} use:renderMath>
|
||||||
|
|
||||||
<!-- Info tab panel -->
|
<!-- Info tab panel -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
|
ssr: {
|
||||||
|
noExternal: ['katex']
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,5 @@ gunicorn==21.2.0
|
||||||
flask-cors==4.0.0
|
flask-cors==4.0.0
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
requests==2.31.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
|
# Markdown rendering
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
MD_EXTENSIONS = ['fenced_code', 'tables', 'nl2br', 'toc']
|
MD_EXTENSIONS = ['fenced_code', 'tables', 'nl2br', 'toc', 'mdx_math']
|
||||||
|
|
||||||
|
|
||||||
def _process_circuit_embeds(text):
|
def _process_circuit_embeds(text):
|
||||||
|
|
@ -398,4 +398,4 @@ def render_home_content():
|
||||||
|
|
||||||
parts = home_content.split('---Available_Lessons---')
|
parts = home_content.split('---Available_Lessons---')
|
||||||
main_content = parts[0] if parts else home_content
|
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