Introducing runio/mde: An RTL-Ready Markdown Editor Bundle for Symfony

Mar 15, 2026 10 min read 90 views
Introducing runio/mde: An RTL-Ready Markdown Editor Bundle for Symfony

Every developer eventually builds something because nothing else quite fits. For me, that something was a Symfony bundle for a rich Markdown editor that works correctly with Arabic (RTL) content. The result is runio/mde, available on Packagist.

This post explains the why and the how. For the broader context β€” why I needed this in the first place β€” see Building a Multilingual Portfolio with Symfony.

What runio/mde Does

At its core, runio/mde wraps Toast UI Editor inside a proper Symfony bundle. Toast UI Editor is a mature, actively maintained JavaScript library that supports:

  • WYSIWYG and raw Markdown editing modes, switchable at runtime
  • Split-view preview (Markdown source left, rendered HTML right)
  • Tables, task lists, code blocks, images, links
  • A clean, accessible toolbar

The bundle adds what the raw library lacks in a Symfony context:

  • Symfony form type (MarkdownEditorType) with all options configurable
  • Twig filter |markdown for rendering stored Markdown on display pages
  • RTL/LTR CSS layer that correctly handles Arabic, Hebrew, Persian, Urdu
  • All assets served locally β€” no uicdn.toast.com, no fonts.googleapis.com
  • Symfony Asset Mapper compatibility
  • Symfony UX Turbo support
  • Mobile-responsive layout via CSS media queries
  • Proper service registration (no manual services.yaml workarounds)

Installation

composer require runio/mde
php bin/console assets:install --symlink

If your project uses Asset Mapper (Symfony 6.3+), add the bundle's asset path to config/packages/asset_mapper.yaml:

framework:
    asset_mapper:
        paths:
            assets/: ''
            vendor/runio/mde/public/: bundles/runiomarkdowneditor/

Then compile:

php bin/console asset-map:compile

Register the form theme in config/packages/twig.yaml:

twig:
    form_themes:
        - '@RunioMarkdownEditor/form/markdown_editor_widget.html.twig'

Using the Form Type

use Runio\MarkdownEditorBundle\Form\Type\MarkdownEditorType;

$builder->add('content', MarkdownEditorType::class, [
    'editor_mode'      => 'wysiwyg',   // wysiwyg | markdown | both
    'rtl_enabled'      => false,
    'language'         => 'en',
    'toolbar'          => 'full',      // full | basic | minimal
    'preview_style'    => 'vertical',  // vertical | tab
    'theme'            => 'light',     // light | dark
    'editor_height'    => '500px',
    'show_mode_switch' => true,
]);

// For Arabic content:
$builder->add('contentAr', MarkdownEditorType::class, [
    'editor_mode'  => 'wysiwyg',
    'rtl_enabled'  => true,
    'language'     => 'ar',
]);

Rendering Stored Markdown

In your Twig templates, use the |markdown filter:

<div class="prose">
    {{ post.content | markdown }}
</div>

The filter runs the content through CommonMark and a sanitization layer that strips unsafe tags and attributes. The sanitization is configurable:

# config/packages/runio_markdown_editor.yaml
runio_markdown_editor:
    sanitization:
        enabled: true
        allowed_tags: [p, br, strong, em, blockquote, ul, ol, li, a, img, h1, h2, h3, h4, h5, h6, pre, code, table, thead, tbody, tr, th, td, hr]
        allowed_attributes: [href, src, alt, title, class, id, dir, lang]

Architecture

The PHP Layer

The bundle exposes three services:

MarkdownParser β€” converts Markdown to HTML using league/commonmark. Configurable via the markdown section of the bundle config:

runio_markdown_editor:
    markdown:
        html_input: escape        # strip | allow | escape
        allow_unsafe_links: false
        enable_table_extension: true
        enable_strikethrough_extension: true
        enable_tasklist_extension: true
        enable_autolink_extension: true

RTLTextDetector β€” detects whether a string contains RTL characters (Arabic, Hebrew, Persian, Urdu Unicode ranges). Used to auto-detect text direction when auto_detect: true.

SanitizationService β€” strips disallowed HTML tags and attributes from parsed output.

MarkdownRuntime β€” the Twig runtime that registers the |markdown and |markdown_inline filters. This is registered as a proper service in the bundle's services.yaml β€” no manual registration required in the application.

The JavaScript Layer

The JavaScript (public/js/markdown-editor.js) is a thin wrapper around toastui.Editor. It:

  1. Checks whether toastui is available before initializing β€” falls back gracefully to a plain textarea if the JS fails to load
  2. Picks responsive height and preview style based on viewport on first load
  3. Syncs the hidden <textarea> value on every editor change and on form submit
  4. Handles image uploads via fetch if an upload URL is configured
  5. Supports Turbo navigation via turbo:load and turbo:before-cache lifecycle events

The editor auto-initializes on any .markdown-editor-rtl-wrapper element found in the DOM, and re-initializes on dynamically added content via MutationObserver.

The CSS Layer

The RTL CSS (public/css/markdown-editor-rtl.css) applies correct styling when the wrapper has dir="rtl":

  • Text alignment and direction for all content regions
  • Blockquote border flipped to the right side
  • List padding flipped to the right side
  • Code blocks always LTR regardless of surrounding direction
  • Splitter (split-view divider) positioned correctly β€” this was the key bug fix

The mobile responsive section handles:

  • Toolbar icon sizing at <768px and <480px
  • Dropdown toolbar constrained to editor width (prevents overflow)
  • Split-view panels stack vertically on mobile
  • Popup positioning corrected for small viewports

The Bugs I Fixed

The previous open-source Symfony integration for Toast UI had two significant issues:

1. Invisible RTL Splitter

In split-view mode with RTL enabled, the dividing line between the Markdown input and the preview was invisible. The cause: a CSS rule adding left: auto; right: 0 to .toastui-editor-md-splitter. This moved the element to left: 859px in an 860px container β€” one pixel off screen. Removing the override lets Toast UI position it correctly at 50%.

2. Unregistered Twig Runtime

The services.yaml in the previous bundle had the MarkdownRuntime service registration commented out. The |markdown filter appeared to exist (the extension was registered) but calling it threw a runtime error. The fix is simply ensuring the service is registered correctly β€” one line in services.yaml.

Multilingual Configuration

The default config targets Arabic content, but any RTL language works:

runio_markdown_editor:
    default_config:
        rtl_enabled: true
        language: ar
    rtl:
        languages: [ar, he, fa, ur]
        auto_detect: true
        arabic_font: 'Noto Naskh Arabic'

Per-field overrides let you mix RTL and LTR editors in the same form, which is exactly what I do in sym-port's blog post form β€” one editor per language, each configured appropriately.

What's Next

The bundle is functional and in active use on my portfolio. Planned improvements:

  • Image upload UI β€” a proper upload dialog rather than requiring an external endpoint
  • Custom toolbar buttons β€” allowing applications to register additional toolbar items
  • Atom feed helper β€” a Twig function for rendering Markdown content in feed contexts

The source is on GitHub and the package is on Packagist under runio/mde. Issues and pull requests welcome.

AM

Author Amer Malik Mohammed

Full-Stack Developer with 2+ years of experience in object-oriented PHP development (Symfony), JavaScript and MySQL. Specialized in e-commerce solutions (Shopware 5/6), REST API development and process automation in agile teams.

Contact author