diff --git a/client/package-lock.json b/client/package-lock.json
index 7c50d39..834e8bf 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@tiptap/extension-image": "^2.27.2",
"@tiptap/extension-link": "^2.27.2",
+ "@tiptap/extension-text-align": "^2.27.2",
"@tiptap/react": "^2.27.2",
"@tiptap/starter-kit": "^2.27.2",
"diff": "^5.2.2",
@@ -1483,6 +1484,19 @@
"@tiptap/core": "^2.7.0"
}
},
+ "node_modules/@tiptap/extension-text-align": {
+ "version": "2.27.2",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.27.2.tgz",
+ "integrity": "sha512-0Pyks6Hu+Q/+9+5/osoSv0SP6jIerdWMYbi13aaZLsJoj3lBj5WNaE11JtAwSFN5sx0IbqhDSlp1zkvRnzgZ8g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
"node_modules/@tiptap/extension-text-style": {
"version": "2.27.2",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.27.2.tgz",
diff --git a/client/package.json b/client/package.json
index 4b1948f..250c40d 100644
--- a/client/package.json
+++ b/client/package.json
@@ -11,6 +11,7 @@
"dependencies": {
"@tiptap/extension-image": "^2.27.2",
"@tiptap/extension-link": "^2.27.2",
+ "@tiptap/extension-text-align": "^2.27.2",
"@tiptap/react": "^2.27.2",
"@tiptap/starter-kit": "^2.27.2",
"diff": "^5.2.2",
diff --git a/client/src/components/RichTextEditor.jsx b/client/src/components/RichTextEditor.jsx
index 0f81773..9d7d233 100644
--- a/client/src/components/RichTextEditor.jsx
+++ b/client/src/components/RichTextEditor.jsx
@@ -3,6 +3,7 @@ import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image'
+import TextAlign from '@tiptap/extension-text-align'
import { api } from '../api/client.js'
// Toolbar button.
@@ -25,6 +26,22 @@ function escapeHtml(s) {
return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c])
}
+// Alignment glyph: three lines justified to the given side.
+function AlignIcon({ align }) {
+ const rows = {
+ left: [[2, 14], [2, 10], [2, 12]],
+ center: [[2, 14], [4, 12], [3, 13]],
+ right: [[2, 14], [6, 14], [4, 14]],
+ }[align]
+ return (
+
+ )
+}
+
// Toolbar variants:
// 'full' — every control, incl. the internal wiki-page link picker (wiki use).
// 'post' — full minus the wiki-page picker (no page-list context in posts).
@@ -42,6 +59,11 @@ export default function RichTextEditor({ value, onChange, pages = [], variant =
StarterKit.configure({ heading: { levels: [2, 3] } }),
Link.configure({ openOnClick: false, autolink: true }),
Image.configure({ inline: false }),
+ // Alignment stored as `text-align` on the block node (heading/paragraph),
+ // so it round-trips through save/reload as inline style. Shared here means
+ // every consumer — post editor, and the future rich_text / two_column
+ // blocks — gets it for free.
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
content: value || '',
onUpdate: ({ editor }) => onChange(editor.getHTML()),
@@ -131,6 +153,16 @@ export default function RichTextEditor({ value, onChange, pages = [], variant =
—
+ editor.chain().focus().setTextAlign('left').run()}>
+
+
+ editor.chain().focus().setTextAlign('center').run()}>
+
+
+ editor.chain().focus().setTextAlign('right').run()}>
+
+
+
🔗
diff --git a/server/src/utils/sanitizeHtml.js b/server/src/utils/sanitizeHtml.js
index 15d57ee..f5d4edc 100644
--- a/server/src/utils/sanitizeHtml.js
+++ b/server/src/utils/sanitizeHtml.js
@@ -18,6 +18,19 @@ const OPTIONS = {
span: ['data-wiki-slug'], // marks internal wiki links (used from Phase 3)
th: ['colspan', 'rowspan'],
td: ['colspan', 'rowspan'],
+ // Block alignment from the rich-text editor. `style` is only honored for the
+ // properties/values whitelisted in allowedStyles below — everything else in
+ // the style attribute is stripped.
+ p: ['style'],
+ h1: ['style'], h2: ['style'], h3: ['style'],
+ h4: ['style'], h5: ['style'], h6: ['style'],
+ },
+ // Restrict inline styles to text-align (left/right/center/justify) only. Any
+ // other CSS property, or an unlisted value, is discarded.
+ allowedStyles: {
+ '*': {
+ 'text-align': [/^(left|right|center|justify)$/],
+ },
},
// http/https for links and images, mailto for links, plus relative URLs so
// uploaded images (/uploads/...) and internal links (/wiki/...) pass through.