diff --git a/docs/frontend/rich-text-editor.md b/docs/frontend/rich-text-editor.md index 81c2b2d2..7d5c0ea6 100644 --- a/docs/frontend/rich-text-editor.md +++ b/docs/frontend/rich-text-editor.md @@ -46,6 +46,7 @@ import { RichTextEditor } from '@/components/markdown/RichTextEditor'; | `className` | `string` | Extra container classes | | `maxLength` | `number` | Shows a counter and truncates input | | `allowImages` | `boolean` | Toggle image insertion (default `true`) | +| `showHtmlToggle` | `boolean` | Show the *HTML* view toggle (default `true`, issue #90) | | `onBlur` | `() => void` | Forwarded to the textarea (react-hook-form support) | | `aria-invalid` / `aria-describedby` | — | Forwarded for validation messaging | @@ -71,8 +72,49 @@ the server-side `POST /api/v1/uploads` endpoint and reference the returned URL. The *Preview* toggle renders the markdown through `MarkdownContent` with sanitization enabled, so raw HTML and unsafe links are stripped before display. +## HTML view and "save as HTML" (issue #90) + +The *HTML* toggle shows the sanitized HTML rendition of the current description +with a **Copy HTML** button. The conversion lives in +`frontend/lib/markdown/to-html.ts` (`markdownToHtml`), and the persisted payload +is built by `frontend/lib/projects/work-description.ts` — both +`/dashboard/projects/new` and its localized variant call +`serializeWorkDescription`, so the on-chain shape stays consistent: + +```ts +serializeWorkDescription({ title, description, repo }); +// => { +// title, +// description, // markdown source of truth (issue #795) +// descriptionHtml, // sanitized HTML rendition (issue #90) +// repo, +// } +``` + +### Why a local serializer + +The preview renders with `react-markdown`, but the project does not depend on a +markdown *stringifier* (`rehype-stringify`), and adding one purely for this would +be a heavier change than the feature warrants. `to-html.ts` therefore covers the +subset the editor can produce — headings, emphasis, strikethrough, inline and +fenced code, links, images, lists, quotes and rules — and is dependency-free. + +Both paths share the same security posture: + +- raw HTML is never passed through — every text run is escaped, so `'); + expect(html).toBe('
<script>alert(1)</script>
'); + expect(html).not.toContain('' }); + + expect(result.descriptionHtml).not.toContain('