diff --git a/package-lock.json b/package-lock.json index e354c78da..f567f49b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "luxon": "^3.7.2", + "markdown-to-jsx": "^9.3.3", "react": "^19.2.0", "react-dom": "^19.2.0", "react-grid-layout": "^1.5.2", @@ -34,7 +35,6 @@ "react-is": "^19.2.0", "react-responsive": "^10.0.1", "react-router-dom": "^7.9.4", - "react-simple-wysiwyg": "^3.4.1", "react-slick": "^0.31.0", "recharts": "^3.4.1", "slick-carousel": "^1.8.1", @@ -9437,6 +9437,34 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-to-jsx": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.7.3.tgz", + "integrity": "sha512-F+1BmeeUKNM7K2eDDaAOyrs1iusNNKbt3YyxYP2Al1Dr1op6hpk3/6wukArwPWh9d9O0C2ybiCTXc6L5CwKIHQ==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "react": ">= 16.0.0", + "solid-js": ">=1.0.0", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-native": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/matcher-collection": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", @@ -10683,15 +10711,6 @@ "react-dom": ">=18" } }, - "node_modules/react-simple-wysiwyg": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/react-simple-wysiwyg/-/react-simple-wysiwyg-3.4.1.tgz", - "integrity": "sha512-amppNS/WiUSURSFXg9evcRjRBwK6pf5LRWDACVZT/q4k2TginjNxegxM0T2s2LYulbRFS4eGwO7nv57OskckHw==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8" - } - }, "node_modules/react-slick": { "version": "0.31.0", "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.31.0.tgz", diff --git a/package.json b/package.json index 74c6018b0..d9530640d 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "luxon": "^3.7.2", + "markdown-to-jsx": "^9.3.3", "react": "^19.2.0", "react-dom": "^19.2.0", "react-grid-layout": "^1.5.2", @@ -44,7 +45,6 @@ "react-is": "^19.2.0", "react-responsive": "^10.0.1", "react-router-dom": "^7.9.4", - "react-simple-wysiwyg": "^3.4.1", "react-slick": "^0.31.0", "recharts": "^3.4.1", "slick-carousel": "^1.8.1", @@ -93,8 +93,8 @@ "start": "vite", "build": "vite build", "serve": "vite preview", - "test": "LANG=de_de jest", - "test:coverage": "LANG=de_de jest --coverage --collectCoverageFrom='!src/pages/**/*.tsx'", + "test": "LANG=de_de jest --testTimeout=15000", + "test:coverage": "LANG=de_de jest --coverage --collectCoverageFrom='!src/pages/**/*.tsx' --testTimeout=15000", "i18n": "i18next", "lint": "eslint src", "lint:quiet": "eslint --quiet src", @@ -116,7 +116,8 @@ "moduleNameMapper": { "^axios$": "/node_modules/axios/dist/node/axios.cjs", "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", - "\\.(css|less)$": "/__mocks__/styleMock.js" + "\\.(css|less)$": "/__mocks__/styleMock.js", + "^react-markdown$": "/__mocks__/react-markdown.js" }, "preset": "ts-jest", "testEnvironment": "jest-environment-jsdom", @@ -128,4 +129,4 @@ ] }, "packageManager": "npm@10.5.0" -} +} \ No newline at end of file diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index db3905428..6447d8c08 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -15,12 +15,14 @@ "timeOfDay": "Time of day", "submit": "Submit", "edit": "Edit", + "preview": "Preview", "editName": "Edit {{name}}", "delete": "Delete", "deleteConfirmation": "Are you sure you want to delete \"{{name}}\"?", "add": "Add", "close": "Close", "difference": "Difference", + "useMarkdownHint": "You can use basic Markdown to format the text: *italic*, **bold**, - list", "days": "Days", "all": "All", "lastYear": "Last Year", diff --git a/src/components/Common/forms/MarkdownEditor.test.tsx b/src/components/Common/forms/MarkdownEditor.test.tsx new file mode 100644 index 000000000..ae5110e3f --- /dev/null +++ b/src/components/Common/forms/MarkdownEditor.test.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MarkdownEditor } from './MarkdownEditor'; +import '@testing-library/jest-dom'; + +describe('MarkdownEditor', () => { + const mockChange = jest.fn(); + + it('renders in Write mode by default', () => { + render(); + // Check for the textarea + expect(screen.getByRole('textbox')).toBeInTheDocument(); + // Check content exists inside textbox + expect(screen.getByDisplayValue('Test content')).toBeInTheDocument(); + }); + + it('calls onChange when typing', () => { + render(); + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'New text' } }); + expect(mockChange).toHaveBeenCalledWith('New text'); + }); + + it('toggles to preview mode and renders basic formatting', () => { + const markdown = "This is **bold** and *italic*"; + render(); + + // Switch to Preview + fireEvent.click(screen.getByText('Preview')); + + // Check that bold and italic tags are rendered + // Note: markdown-to-jsx might use or , check your overrides. + // We overrode 'strong' to 'strong', so we look for that. + const boldElement = screen.getByText('bold'); + expect(boldElement.tagName).toBe('STRONG'); + + const italicElement = screen.getByText('italic'); + expect(italicElement.tagName).toBe('EM'); + }); + + it('blocks Heading tags (h1-h6) and renders them as plain text/paragraphs', () => { + // We provide a # Heading. + // Expected result: Text "Forbidden Heading" is visible, but NOT inside an

tag. + render(); + + fireEvent.click(screen.getByText('Preview')); + + // 1. The text should still be readable + expect(screen.getByText('Forbidden Heading')).toBeInTheDocument(); + + // 2. But there should be NO heading role in the document + const heading = screen.queryByRole('heading', { level: 1 }); + expect(heading).toBeNull(); + }); + + it('blocks Link tags (a) and renders plain text', () => { + const markdown = "[Malicious Link](http://evil.com)"; + render(); + + fireEvent.click(screen.getByText('Preview')); + + // 1. The text anchor should be visible + expect(screen.getByText('Malicious Link')).toBeInTheDocument(); + + // 2. But it should NOT be a link (no anchor tag) + const link = screen.queryByRole('link'); + expect(link).toBeNull(); + + // 3. Ensure the href is not present in the DOM (security check) + const textElement = screen.getByText('Malicious Link'); + expect(textElement).not.toHaveAttribute('href'); + }); + + it('blocks Images', () => { + // Image syntax: ![alt text](url) + render(); + + fireEvent.click(screen.getByText('Preview')); + + // Ensure the image tag is not rendered + const img = screen.queryByRole('img'); + expect(img).toBeNull(); + }); +}); \ No newline at end of file diff --git a/src/components/Common/forms/MarkdownEditor.tsx b/src/components/Common/forms/MarkdownEditor.tsx new file mode 100644 index 000000000..e2a4e9318 --- /dev/null +++ b/src/components/Common/forms/MarkdownEditor.tsx @@ -0,0 +1,77 @@ +import { Box, Button, ButtonGroup, Paper, TextField, Typography } from '@mui/material'; +import Markdown from 'markdown-to-jsx'; +import React, { useState } from 'react'; +import { useTranslation } from "react-i18next"; +import { MarkdownOptions } from "utils/markdown"; + + +interface MarkdownEditorProps { + value: string; + onChange: (value: string) => void; + label?: string; + error?: boolean; + helperText?: string; +} + +export const MarkdownEditor: React.FC = ({ + value, + onChange, + label = "Description", + error, + helperText + }) => { + const [isPreview, setIsPreview] = useState(false); + const [t] = useTranslation(); + + return ( + + + + {label} + + + + + + + + {isPreview ? ( + + {/* The library handles the parsing based on our secure options */} + + {value || '*No content*'} + + + ) : ( + onChange(e.target.value)} + error={error} + helperText={helperText} + placeholder={t('useMarkdownHint')} + /> + )} + + ); +}; \ No newline at end of file diff --git a/src/components/Exercises/Add/Step3Description.tsx b/src/components/Exercises/Add/Step3Description.tsx index bbcab4451..46761aec0 100644 --- a/src/components/Exercises/Add/Step3Description.tsx +++ b/src/components/Exercises/Add/Step3Description.tsx @@ -3,11 +3,10 @@ import Grid from '@mui/material/Grid'; import { useLanguageCheckQuery } from "components/Core/queries"; import { StepProps } from "components/Exercises/Add/AddExerciseStepper"; import { PaddingBox } from "components/Exercises/Detail/ExerciseDetails"; -import { ExerciseDescription } from "components/Exercises/forms/ExerciseDescription"; +import { MarkdownEditor } from "components/Common/forms/MarkdownEditor"; import { ExerciseNotes } from "components/Exercises/forms/ExerciseNotes"; import { descriptionValidator, noteValidator } from "components/Exercises/forms/yupValidators"; import { Form, Formik } from "formik"; -import React from "react"; import { useTranslation } from "react-i18next"; import { useExerciseSubmissionStateValue } from "state"; import { setDescriptionEn, setNotesEn } from "state/exerciseSubmissionReducer"; @@ -59,38 +58,46 @@ export const Step3Description = ({ onContinue, onBack }: StepProps) => { }} > -
- - + {({ values, errors, touched, setFieldValue }) => ( + + + setFieldValue('description', val)} + error={touched.description && Boolean(errors.description)} + helperText={touched.description ? errors.description : undefined} + /> - + - + - - - -
- - -
-
+ + + +
+ + +
+
+
-
-
- +
+ + )} ) ); -}; +}; \ No newline at end of file diff --git a/src/components/Exercises/Add/Step4Translations.tsx b/src/components/Exercises/Add/Step4Translations.tsx index 02d518d6d..06b57b71e 100644 --- a/src/components/Exercises/Add/Step4Translations.tsx +++ b/src/components/Exercises/Add/Step4Translations.tsx @@ -11,12 +11,12 @@ import { Switch } from "@mui/material"; import Grid from '@mui/material/Grid'; +import { MarkdownEditor } from "components/Common/forms/MarkdownEditor"; import { LoadingWidget } from "components/Core/LoadingWidget/LoadingWidget"; import { useLanguageCheckQuery } from "components/Core/queries"; import { StepProps } from "components/Exercises/Add/AddExerciseStepper"; import { PaddingBox } from "components/Exercises/Detail/ExerciseDetails"; import { ExerciseAliases } from "components/Exercises/forms/ExerciseAliases"; -import { ExerciseDescription } from "components/Exercises/forms/ExerciseDescription"; import { ExerciseName } from "components/Exercises/forms/ExerciseName"; import { ExerciseNotes } from "components/Exercises/forms/ExerciseNotes"; import { @@ -147,7 +147,13 @@ export const Step4Translations = ({ onContinue, onBack }: StepProps) => { - + formik.setFieldValue('description', val)} + error={formik.touched.description && Boolean(formik.errors.description)} + helperText={formik.touched.description ? formik.errors.description : undefined} + /> diff --git a/src/components/Exercises/Add/Step6Overview.tsx b/src/components/Exercises/Add/Step6Overview.tsx index 3a3fa455c..e4e5897a4 100644 --- a/src/components/Exercises/Add/Step6Overview.tsx +++ b/src/components/Exercises/Add/Step6Overview.tsx @@ -25,12 +25,14 @@ import { useMusclesQuery } from "components/Exercises/queries"; import { useProfileQuery } from "components/User/queries/profile"; +import Markdown from 'markdown-to-jsx'; import React from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { postExerciseImage } from "services"; import { useExerciseSubmissionStateValue } from "state"; import { ENGLISH_LANGUAGE_ID } from "utils/consts"; +import { MarkdownOptions } from "utils/markdown"; import { makeLink, WgerLink } from "utils/url"; export const Step6Overview = ({ onBack }: StepProps) => { @@ -123,7 +125,11 @@ export const Step6Overview = ({ onBack }: StepProps) => { {t('description')} - {state.descriptionEn} + + + {state.descriptionEn} + + {t('exercises.notes')} @@ -193,7 +199,11 @@ export const Step6Overview = ({ onBack }: StepProps) => { {t('description')} - {state.descriptionI18n} + + + {state.descriptionI18n} + + diff --git a/src/components/Exercises/Detail/ExerciseDetailEdit.test.tsx b/src/components/Exercises/Detail/ExerciseDetailEdit.test.tsx index 829c5ed34..9f6d74d9c 100644 --- a/src/components/Exercises/Detail/ExerciseDetailEdit.test.tsx +++ b/src/components/Exercises/Detail/ExerciseDetailEdit.test.tsx @@ -149,7 +149,8 @@ describe("Exercise translation edit tests", () => { id: 9, languageId: 1, author: "", - description: "Die Kniebeuge ist eine Übung zur Kräftigung der Oberschenkelmuskulatur", + description: "", + descriptionSource: "Die Kniebeuge ist eine Übung zur Kräftigung der Oberschenkelmuskulatur", name: "Mangalitza" }); }); diff --git a/src/components/Exercises/Detail/ExerciseDetailEdit.tsx b/src/components/Exercises/Detail/ExerciseDetailEdit.tsx index 4d91ad2c7..4be7d5572 100644 --- a/src/components/Exercises/Detail/ExerciseDetailEdit.tsx +++ b/src/components/Exercises/Detail/ExerciseDetailEdit.tsx @@ -6,7 +6,7 @@ import { PaddingBox } from "components/Exercises/Detail/ExerciseDetails"; import { EditExerciseCategory } from "components/Exercises/forms/Category"; import { EditExerciseEquipment } from "components/Exercises/forms/Equipment"; import { ExerciseAliases } from "components/Exercises/forms/ExerciseAliases"; -import { ExerciseDescription } from "components/Exercises/forms/ExerciseDescription"; +import { MarkdownEditor } from "components/Common/forms/MarkdownEditor"; import { ExerciseName } from "components/Exercises/forms/ExerciseName"; import { AddImageCard, ImageEditCard } from "components/Exercises/forms/ImageCard"; import { EditExerciseMuscle } from "components/Exercises/forms/Muscle"; @@ -85,7 +85,7 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => { const isNewTranslation = language.id !== translationFromBase?.language; const exerciseTranslation = isNewTranslation - ? new Translation(null, null, '', '', language.id) + ? new Translation(null, null, '', '', language.id, [], [], [], undefined) : translationFromBase; const exerciseEnglish = exercise.getTranslation(); @@ -100,29 +100,26 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => { initialValues={{ name: exerciseTranslation.name, alternativeNames: exerciseTranslation.aliases.map(a => ({ id: a.id, alias: a.alias })), - description: exerciseTranslation.description, + // Fallback: Use Source if available, else HTML (legacy), else empty + description: exerciseTranslation.descriptionSource || exerciseTranslation.description || '', }} enableReinitialize validationSchema={validationSchema} onSubmit={async values => { // Exercise translation + const payload = { + exerciseId: exercise.id!, + languageId: language.id, + name: values.name, + description: '', + descriptionSource: values.description, + author: '' + }; + const translation = exerciseTranslation.id - ? await editTranslationQuery.mutateAsync({ - id: exerciseTranslation.id, - exerciseId: exercise.id!, - languageId: language.id, - name: values.name, - description: values.description, - author: '' - }) - : await addTranslationQuery.mutateAsync({ - exerciseId: exercise.id!, - languageId: language.id, - name: values.name, - description: values.description, - author: profileQuery.data!.username - }); + ? await editTranslationQuery.mutateAsync({ ...payload, id: exerciseTranslation.id }) + : await addTranslationQuery.mutateAsync({ ...payload, author: profileQuery.data!.username }); // Alias handling const aliasOrig = (exerciseTranslation.aliases).map(a => ({ id: a.id, alias: a.alias })); @@ -147,127 +144,114 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => { setAlertIsVisible(true); }} > -
- - {alertIsVisible && + {({ values, touched, errors, setFieldValue }) => ( + + + {alertIsVisible && + + { + setAlertIsVisible(false); + }} + > + + + } + > + {t('exercises.successfullyUpdated')} + + + } + + + {t('translation')} + + + {t('English')} + + + + {language.nameLong} ({language.nameShort}) + + - { - setAlertIsVisible(false); - }} - > - - - } - > - {t('exercises.successfullyUpdated')} - - } - - - {t('translation')} - - - {t('English')} - - - - {language.nameLong} ({language.nameShort}) - - - - - {t('name')} - - - {exerciseEnglish.name} -
    - {exerciseEnglish.aliases.map((alias) => ( -
  • {alias.alias}
  • - ))} -
-
- - - - - - - - - - - - - {t('exercises.description')} - - -
- - - - - - {editExercisePermissionQuery.data && <> + {t('name')} + + + {exerciseEnglish.name} +
    + {exerciseEnglish.aliases.map((alias) => ( +
  • {alias.alias}
  • + ))} +
+
+ + + + + + + + - {t('nutrition.others')} + {t('exercises.description')} - + {/* English/Base Description (Read Only) */} + English Description (Reference) +
- e.id)} /> + {/* Markdown Editor */} + setFieldValue('description', val)} + error={touched.description && Boolean(errors.description)} + // FIXED: Use ternary to ensure we return undefined instead of false + helperText={touched.description ? errors.description : undefined} + /> - } - - {/* - - - - - {t('exercises.notes')} - - -
    - {exerciseEnglish.notes.map((note: Note) => ( -
  • {note.note}
  • - ))} -
-
- -
    - {exerciseTranslation.notes.map((note: Note) => ( -
  • {note.note}
  • - ))} -
-
- */} + {editExercisePermissionQuery.data && <> + + + + + {t('nutrition.others')} + + + + + + e.id)} /> + + } - - - + + + + - - + + )} {/* Images */} @@ -302,7 +286,7 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => { {exercise.videos.map(video => ( + canDelete={deleteVideoPermissionQuery.data!} /> ))} @@ -353,4 +337,4 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => { } ; -}; +}; \ No newline at end of file diff --git a/src/components/Exercises/forms/ExerciseDescription.tsx b/src/components/Exercises/forms/ExerciseDescription.tsx deleted file mode 100644 index f528e8047..000000000 --- a/src/components/Exercises/forms/ExerciseDescription.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from "react"; -import { useField } from "formik"; -import { FormHelperText } from "@mui/material"; -import { - BtnBold, - BtnBulletList, - BtnItalic, - BtnNumberedList, - BtnRedo, - BtnUnderline, - BtnUndo, - Editor, - EditorProps, - EditorProvider, - Separator, - Toolbar, -} from "react-simple-wysiwyg"; - -export function ExerciseEditor(props: EditorProps) { - return ( - - - - - - - - - - - - - { /* */} - - - - ); -} - - -export function ExerciseDescription(props: { fieldName: string }) { - - const [field, meta, helpers] = useField(props.fieldName); - - return <> -
- helpers.setValue(newValue.target.value)} - /> -
- {meta.touched - && Boolean(meta.error) - && {meta.error} - } - ; -} \ No newline at end of file diff --git a/src/components/Exercises/models/translation.test.ts b/src/components/Exercises/models/translation.test.ts index f44540831..f79f6d062 100644 --- a/src/components/Exercises/models/translation.test.ts +++ b/src/components/Exercises/models/translation.test.ts @@ -33,6 +33,7 @@ describe("Exercise translation model tests", () => { language: 1, notes: [], aliases: [], + description_source: undefined, // eslint-disable-next-line camelcase author_history: ['author1', 'author2', 'author3'] })).toStrictEqual(e1); @@ -48,6 +49,7 @@ describe("Exercise translation model tests", () => { name: "a very long name that should be truncated", description: "description", language: 1, + description_source: undefined, }); }); diff --git a/src/components/Exercises/models/translation.ts b/src/components/Exercises/models/translation.ts index cf1b28994..52737cd4d 100644 --- a/src/components/Exercises/models/translation.ts +++ b/src/components/Exercises/models/translation.ts @@ -12,13 +12,14 @@ export class Translation { authors: string[] = []; constructor(public id: number | null, - public uuid: string | null, - public name: string, - public description: string, - public language: number, - notes?: Note[], - aliases?: Alias[], - authors?: string[] + public uuid: string | null, + public name: string, + public description: string, + public language: number, + notes?: Note[], + aliases?: Alias[], + authors?: string[], + public descriptionSource?: string ) { if (notes) { this.notes = notes; @@ -62,7 +63,8 @@ export class TranslationAdapter implements Adapter { item.notes?.map((e: any) => (new NoteAdapter().fromJson(e))), // eslint-disable-next-line @typescript-eslint/no-explicit-any item.aliases?.map((e: any) => (new AliasAdapter().fromJson(e))), - item.author_history + item.author_history, + item.description_source ); } @@ -78,6 +80,7 @@ export class TranslationAdapter implements Adapter { name: item.name, description: item.description, language: item.language, + description_source: item.descriptionSource, }; } } \ No newline at end of file diff --git a/src/services/exercise.ts b/src/services/exercise.ts index 79b6bc12d..4086c2e2d 100644 --- a/src/services/exercise.ts +++ b/src/services/exercise.ts @@ -111,20 +111,16 @@ export const addFullExercise = async (data: AddExerciseFullProps): Promise ({ name: t.name, - description: t.description, + "description_source": t.description, language: t.language, - //eslint-disable-next-line camelcase - license_author: data.author ?? '', + "license_author": data.author ?? '', aliases: t.aliases ?? [], comments: t.comments ?? [] }) diff --git a/src/services/exerciseTranslation.ts b/src/services/exerciseTranslation.ts index d90ee41a7..c9a4e99ee 100644 --- a/src/services/exerciseTranslation.ts +++ b/src/services/exerciseTranslation.ts @@ -63,10 +63,11 @@ export interface AddTranslationParams { name: string; description: string; author: string; + descriptionSource?: string; } export const addTranslation = async (params: AddTranslationParams): Promise => { - const { exerciseId, languageId, name, description, author } = params; + const { exerciseId, languageId, name, description, author, descriptionSource } = params; const url = makeUrl(EXERCISE_TRANSLATION_PATH); const baseData = { @@ -74,6 +75,7 @@ export const addTranslation = async (params: AddTranslationParams): Promise => { - const { id, exerciseId, languageId, name, description } = data; + const { id, exerciseId, languageId, name, description, descriptionSource } = data; const url = makeUrl(EXERCISE_TRANSLATION_PATH, { id: id }); const baseData = { @@ -101,6 +103,7 @@ export const editTranslation = async (data: EditTranslationParams): Promise {children}; + +const StripBlock = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +/* + * Custom options for markdown-to-jsx to ensure that only allowed HTML tags are rendered + * (note that there's still server side filtering for this) + */ +export const MarkdownOptions: MarkdownToJSX.Options = { + overrides: { + p: { + component: Typography, + props: { variant: 'body1', sx: { mb: 1.5 } } + }, + + // Allowed inline/list tags + strong: { component: 'strong' }, + b: { component: 'b' }, + em: { component: 'em' }, + i: { component: 'i' }, + ul: { component: 'ul', props: { style: { paddingLeft: '20px', margin: '0 0 16px 0' } } }, + ol: { component: 'ol', props: { style: { paddingLeft: '20px', margin: '0 0 16px 0' } } }, + li: { component: 'li' }, + + // Block links and headings by mapping them to plan spans. + a: { component: StripElement }, + + // --- BLOCKED TAGS (No headings allowed) --- + h1: { component: StripBlock }, + h2: { component: StripBlock }, + h3: { component: StripBlock }, + h4: { component: StripBlock }, + h5: { component: StripBlock }, + h6: { component: StripBlock }, // Tables, images, etc. are naturally ignored by markdown-to-jsx if not explicitly defined + + img: { component: () => null }, + + table: { component: 'div' }, + }, +}; \ No newline at end of file