Skip to content

feat: WYSIWYG editors via WordPress TinyMCE with server-side markdown conversion - #46

Merged
Robbertvermeulen merged 3 commits into
mainfrom
feat/wysiwyg-editor
Sep 21, 2026
Merged

Robbertvermeulen merged 3 commits into
mainfrom
feat/wysiwyg-editor

Conversation

@Robbertvermeulen

Copy link
Copy Markdown
Contributor

Topic body and comment composer become real WYSIWYG editors using WordPress's bundled TinyMCE (wp.editor API, no new JS dependency). Topics: Paragraph/H3/H4 + bold, italic, lists, link, code. Comments: bold, italic, lists, link (no headings). The hub pipeline stays markdown: a whitelist-based HtmlToMarkdown converter (with Turndown-style escaping of markdown-significant characters in literal text) runs server-side in the proxies and doubles as sanitization — disallowed tags, attributes and URL schemes are stripped.

Reviewed (branch review + scoped re-review, XSS paths verified clean) and browser-verified end-to-end: formatted submissions arrive as clean markdown; the wp-link dialog stacks above the SDK modal.

Tests: PHP 220/220, JS 154/154, phpcs + phpstan clean; bundle rebuilt and committed.

🤖 Generated with Claude Code

Robbertvermeulen and others added 3 commits September 20, 2026 17:28
…_enqueue_editor

The New-topic body field and comment composer become real WYSIWYG editors on
top of WordPress's bundled TinyMCE (wp.editor). The hub pipeline stays
markdown-only, so the editor's limited HTML is converted server-side before
anything reaches the hub — a later UI task builds the editor itself.

- Page::enqueue() now calls wp_enqueue_editor() on the Community page so
  wp.editor/TinyMCE is available to the JS app.
- New EpicWP\Roundtable\Html\HtmlToMarkdown: converts a limited, allow-listed
  HTML tag set (p, br, strong/b, em/i, ul/ol/li with indented nesting,
  a[href] restricted to http/https/mailto, code, pre, h3, h4, blockquote) to
  markdown. Every other tag is unwrapped (text content kept); script/style
  are dropped entirely, including their content. Pure PHP (DOMDocument, no
  Composer dependency), never fatals — malformed input falls back to
  stripped plain text, and input with no HTML markers passes through
  byte-for-byte unchanged.
- TopicController and CommentsController::handleCreate now run the body
  through HtmlToMarkdown::convert() before calling the HubClient. Validation
  still checks the ORIGINAL string's non-emptiness (unchanged semantics);
  the hub keeps enforcing size limits on the converted result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(wp.editor)

UI half of the WYSIWYG scope, on top of the PHP plumbing (wp_enqueue_editor(),
HtmlToMarkdown server-side conversion). Both composers now submit the editor's
raw HTML instead of hand-typed markdown; the proxies convert it server-side.

- New assets/src/wysiwyg-editor.jsx: a shared WysiwygEditor wraps `wp.editor`
  (classic TinyMCE) to match the plain-textarea value/onInput contract used
  elsewhere. Uncontrolled after mount like any native rich-text widget — `value`
  seeds the initial content only, every edit reports back via onInput(html), and
  a changing `resetNonce` clears the editor in place (for the comment composer,
  after a successful post) without unmounting it. Falls back to a plain
  controlled textarea when window.wp.editor is unavailable. Also exports
  `htmlHasText()` (strips tags/&nbsp; to decide whether an editor is
  meaningfully empty — an "empty" TinyMCE doc is usually `<p></p>`, not `''`)
  and `uniqueEditorId()` (so the New-topic dialog and a comment composer can
  coexist without id collisions).
- publish-dialog.jsx (direct mode only — the chat-drafted review flow is
  untouched): body field is now a WysiwygEditor with
  `formatselect bold italic bullist numlist link code` and
  block_formats limited to Paragraph/Heading/Subheading, ~220px tall. Submit
  gating now checks htmlHasText(summary) instead of a raw truthiness check.
- comment-editor.jsx: replaced the hand-rolled Write/Preview tabs + B/I/code/
  link/list/quote/fence toolbar with a WysiwygEditor restricted to
  `bold italic bullist numlist link` — no headings/format select. The
  moderation-notice/disabled-submit behavior for pending topics is unchanged
  (submitDisabled still only gates the submit button; the editor still
  renders). Dropped the Cmd/Ctrl+Enter submit shortcut and the "Markdown is
  supported" hint along with the markdown toolbar they described — not
  requested, and the shortcut would need separate iframe-level keydown
  plumbing TinyMCE doesn't give for free.
- styles.css: removed the now-orphaned hand-rolled-toolbar CSS
  (rt-ed-tools/rt-ed-tool/rt-ed-sep/rt-ed-grow/rt-ed-md/rt-ed-preview); kept
  rt-ed-top/rt-ed-tabs/rt-ed-tab/rt-ed-preview-empty (still used by
  publish-dialog's unchanged review-mode markdown editor). Added a defensive,
  unscoped z-index rule for TinyMCE's floating panels and the "Insert/edit
  link" dialog (rendered as direct children of <body>, outside
  #roundtable-app) so they aren't hidden behind our modal overlay
  (z-index:100000) — applied proactively since this couldn't be verified in a
  live browser from this environment; covers both the TinyMCE 4 (mce-*) skin
  WordPress bundles today and 5/6 (tox-*) skins.
- api.js: corrected postComment's now-stale "markdown" doc comment.

Tests: node's test runner has no TinyMCE, so only what's statically testable —
htmlHasText/uniqueEditorId (pure functions), WysiwygEditor's fallback vs.
seed-textarea branch selection, and CommentEditor's submit-disabled logic on
empty/real content. Updated topic-detail.test.mjs's assertions for the
now-removed Write/Preview/toolbar markup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xt runs

Critical from branch review: ordinary WYSIWYG prose is always wrapped in
<p>, so it always took the DOM path — but literal text like
"- I tried updating..." or "# 1 priority" or "1. First try..." or "Run
`npm install` first." passed straight through renderText() unescaped, so
the downstream markdown renderer reinterpreted the user's literal
characters as a bullet list, a heading, an ordered list, and inline code,
respectively.

Turndown-style fix: every literal text run is now escaped so
markdown-significant characters render as what was typed, not as syntax —
leading-of-line constructs (#, -, +, >, digit. or digit)) only at the very
start of a text node, and inline constructs (backtick, *, _, [, ], \)
wherever they appear. Text inside code/pre is exempt (already protected by
its own backticks/fence) via a new ancestor check.

Coherent contract: this is now applied on every exit path of convert() —
the DOM render, the malformed-input plain-text fallback, and the
no-markup-to-parse fast path — documented in the class docblock. The fast
path used to return truly byte-for-byte-unchanged text; it now escapes too,
since plain text with no `<`/`&` can still contain the same
markdown-significant characters.

Tests: one per repro (leading dash/heading/ordered-list/backtick), plus
leading +/>, inline */_/[]/backslash, a non-leading dash left alone, the
fast-path/DOM-path consistency case, code/pre content NOT escaped, the
malformed-HTML fallback also escaping, and a regression check that
toolbar-produced bold/list/link markdown still converts normally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Robbertvermeulen
Robbertvermeulen merged commit 6ffd2f3 into main Sep 21, 2026
2 checks passed
@Robbertvermeulen
Robbertvermeulen deleted the feat/wysiwyg-editor branch September 21, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant