Skip to content

Project: Markdown editor

This project builds a side-by-side markdown editor with live preview — a useful tool that ties together the file I/O patterns from Module 3 with the HtmlWindow display from lesson 4.4.

Note: in this module we’re using the simple Wx::HTML window, we will enhance the project in the next module using the more advanced Wx::Webview control

Type markdown on the left. The preview on the right updates automatically when you pause. Open and save .md files. Export the rendered HTML.

markdown_editor.png

To run the program you’ll need to install the kramdown parser (this is one of several available markdown parsers for ruby). Check the Gemfile in the project root to see what’s been added.

1
2
bundle install
ruby main.rb

Download markdown_editor.zip

File structure

markdown_editor/
├── main.rb
├── Gemfile
└── lib/
    ├── editor_frame.rb
    ├── models/
    │   └── markdown_document.rb
    └── panels/
        ├── editor_panel.rb
        └── preview_panel.rb

The model

MarkdownDocument follows the same pattern as the Document class in lesson 3.4 — it holds file path, content, and dirty state, with load, save, and save_as methods. The only addition is to_html and to_full_html, which convert the markdown content using kramdown:

1
2
3
def to_html
  Kramdown::Document.new(@content, input: 'GFM').to_html
end

input: 'GFM' selects the GitHub Flavoured Markdown parser, which adds support for fenced code blocks, tables, and strikethrough. The model knows nothing about widgets — it is pure Ruby.

The editor panel

A Wx::TextCtrl with a monospace font and Wx::TE_DONTWRAP to prevent line wrapping. The callback pattern from lesson 3.4 applies: evt_key_up is registered directly on the @editor widget (not the parent panel), and calls the on_change callback when the user types.

The monospace font is important for markdown editing — it makes heading markers, list indicators, and code fences easy to read in the source.

The preview panel

A Wx::HTML::HtmlWindow with set_standard_fonts(13) for a readable base size. The update(html) method calls set_page to replace the displayed content. Link clicks are intercepted — external http links open in the default browser; other links are ignored.

The frame

The frame coordinates the two panels and handles all file operations. Two details worth highlighting:

Debounced preview updates

Converting markdown and updating the preview on every single keystroke would cause noticeable lag for long documents. Instead, the app uses a one-shot timer as a debounce:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
PREVIEW_DELAY_MS = 300

def on_text_changed
  return if @loading
  @document.update_content(@editor.content)
  update_title
  @preview_timer.start(PREVIEW_DELAY_MS, Wx::TIMER_ONE_SHOT)
end

def update_preview
  @preview.update(@document.to_full_html)
end

Every keypress restarts the timer. The preview only updates when the user pauses for 300ms. Wx::TIMER_ONE_SHOT means the timer fires once and stops — it does not repeat.

The @loading flag

Setting @editor.content = programmatically triggers evt_key_up which would call on_text_changed and mark the document dirty. The @loading flag suppresses this — the same pattern established in lesson 3.4.

1
2
3
@loading = true
@editor.content = @document.content
@loading = false

HtmlWindow limitations visible here

Open the sample content and look at the table and the code block. They render, but:

  • Code blocks have no syntax highlighting
  • The table has basic styling only — no alternating row colours, no CSS
  • Font choices are limited

In Module 5 we will replace PreviewPanel with a WebView-based equivalent. The model and editor panel stay exactly the same — only the preview changes. That comparison will show precisely what WebView adds over HtmlWindow.

What this project demonstrates

Every concept from Module 4 either appears directly or is referenced:

  • Device contexts — the monospace editor font is set exactly as in lesson 4.1
  • HtmlWindow — the preview panel, with link interception from lesson 4.4
  • Debounced timer — a new pattern: Wx::TIMER_ONE_SHOT for deferred updates
  • Module 3 patterns — multi-file structure, model/panel separation, @loading flag, file dialogs, dirty state, confirm on close

Previous: HtmlWindow | Next: Module 5 — WebView