A responsive, browser-based Markdown editor with live HTML preview. This project implements a lightweight Markdown-to-HTML converter using pure JavaScript, with syntax highlighting, themes, and local storage functionality. Try it out: https://mosioc.github.io/markdown-to-html/.
- Project Overview
- Project Structure & Architecture
- Technical Challenges & Solutions
- Code Quality Principles
- Requirements
- Getting Started
- Deployment
- Future Enhancements
- Regex Cheat Sheet
- PS
This project allows users to:
- Write Markdown in a text editor
- See the HTML result in real-time
- Toggle between light and dark themes
- Save content automatically to localStorage
- Track editing history with undo/redo functionality (each time)
- Count words in the document
- View syntax-highlighted code blocks in the preview
- Headings (H1-H3)
- Text formatting (bold, italic)
- Ordered and unordered lists
- Hyperlinks
- Code blocks (multiline + inline)
markdown-to-html/
├── css/
│ └── style.css # Main stylesheet for the application
├── js/
│ ├── app.js # Main application script
│ └── markdownParser.js # Markdown conversion logic
├── utils/
│ ├── historyManager.js # State history for undo/redo
│ ├── localStorageHelper.js # Local storage operations
│ ├── themeManager.js # Theme switching functionality
│ └── wordCounter.js # Word counting utilities
├── index.html # Main HTML file
├── config.example.js # Configuration template
└── README.md # Project documentation
The project follows a modular JavaScript architecture with clean separation of concerns. Each component is responsible for a specific functionality and communicates with other components through well-defined interfaces.
- Separation of Concerns: Each JavaScript module handles a distinct responsibility.
- Modularity: All functionality is encapsulated in purpose-built modules.
- Minimal Dependencies: Modules have clear, minimal dependencies on each other.
- Clean Interfaces: Components interact through simple, well-defined interfaces.
- DRY Principle: Common functionality is abstracted to avoid repetition.
The central orchestrator that initializes the application, sets up event listeners, and coordinates between modules. It:
- Initializes the application when the DOM is loaded
- Manages DOM element references
- Coordinates interactions between different components
- Sets up event handling for user interactions
A pure function-based module that transforms Markdown text into HTML:
- Uses regex patterns to identify Markdown syntax
- Applies transformations to convert Markdown to HTML
- Handles special cases like code blocks with language detection
- Maintains a sequential transformation approach
Specialized functionality is separated into utility modules:
Theme Manager (themeManager.js)
- Handles light/dark theme switching
- Detects system preferences
- Persists theme choices in localStorage
- Updates UI elements when themes change
Local Storage Helper (localStorageHelper.js)
- Abstracts storage operations behind a clean API
- Handles storage errors gracefully
- Uses configurable keys from config.js
History Manager (historyManager.js)
- Implements the state management pattern
- Provides undo/redo functionality
- Maintains an efficient history stack
- Prevents duplicate state entries
Word Counter (wordCounter.js)
- Counts words while ignoring markdown syntax
- Excludes code blocks from counting
- Provides utility for updating the UI
The application follows a unidirectional data flow:
- User Input → User types in the Markdown editor
- Event Handling → Input events trigger state updates
- State Processing → Changes are processed (saved to history/storage)
- Markdown Processing → Text is converted to HTML
- DOM Updates → UI is updated with new content
- Side Effects → Word count is updated, highlighting is applied
This approach creates a predictable and maintainable application state.
The parsing engine uses carefully crafted regular expressions to identify Markdown syntax and convert it to equivalent HTML. Each pattern is processed sequentially to avoid conflicts between similar patterns.
// Example of the regex-based Markdown parser
html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
html = html.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>');
The interface is fully responsive with Bootstrap integration and custom CSS media queries:
- Desktop: Side-by-side editor and preview
- Tablet: Adjusted spacing and button layout
- Mobile: Stacked interface with optimized controls
The theme engine uses CSS variables, Bootstrap or class-based styling that can be toggled via JavaScript:
export const setTheme = (theme) => {
document.body.className = theme;
localStorage.setItem(THEME_KEY, theme);
// Update UI elements
};
Problem: Handling nested elements (e.g., lists within lists) proved difficult with simple regex.
Solution: Implemented a sequential transformation approach that first converts individual elements and then wraps related elements in container tags.
Problem: Implementing undo/redo (history) functionality without excessive memory usage.
Solution: Created a simple state management system that tracks document changes efficiently and maintains a history stack with controlled growth.
Problem: Accurately counting words while ignoring markdown syntax and code blocks.
Solution: Developed a specialized word counter that strips code blocks and markdown syntax before counting.
export const countWords = (text) => {
// Remove code blocks before counting
let cleanText = text
.replace(/```([\s\S]*?)```/g, '')
.replace(/`([^`]+)`/g, '');
return cleanText.trim().split(/\s+/).filter(Boolean).length;
};
Problem: Ensuring consistent behavior across different browsers, especially with module imports.
Solution: Used a development server (http-server; NodeJS NPM) to properly serve modules and avoid CORS issues.
This project adheres to several code quality principles:
- Single Responsibility Principle: Each module does one thing and does it well
- Immutability: Functions avoid modifying their inputs directly
- Defensive Programming: Code validates inputs and handles errors gracefully
- Progressive Enhancement: Core functionality works even if some features aren't available
- Accessibility: ARIA attributes and semantic HTML ensure accessibility
- Performance Optimization: Efficient DOM operations and event handling
Modern browsers block JavaScript module imports (import { } from ...) when opening an HTML file directly from the filesystem (file://). This is due to CORS (Cross-Origin Resource Sharing) restrictions.
-
Install Node.js (includes
npm
) -
Install
http-server
globally using:npm install -g http-server
-
Open a terminal and navigate to the project folder containing
index.html
:cd path/to/your/project
-
Start the local server:
http-server
-
After running
http-server
, you will see output like:Starting up http-server, serving ./ Available on: http://127.0.0.1:8080 http://192.168.x.x:8080
-
Open
http://127.0.0.1:8080/index.html
OR anything else displayed in terminal in your browser. (Ctrl + Click
)
-
To use a specific port (e.g.,
3000
), run:http-server -p 3000
Important
NOTICE: USE http-server -c-1
TO PREVENT CACHING.
This project is automatically deployed to GitHub Pages on every push to the main branch using GitHub Actions.
You can view the live version at: https://mosioc.github.io/markdown-to-html/
Planned features for future versions:
- Full, custom syntax highlighting in the editor
- Support for more Markdown features (tables, checkboxes)
- Export functionality (PDF, ...)
- Dev-side: Add JSDoc to JS files for better documentation
Markdown Syntax | Regex Pattern | Description |
---|---|---|
# Heading |
^# (.*)$ |
h1 |
## Heading |
^## (.*)$ |
h2 |
### Heading |
^### (.*)$ |
h3 |
**bold** |
\*\*(.*?)\*\* |
bold |
*italic* |
\*(.*?)\* |
italic |
[text](url) |
\[(.*?)\]\((.*?)\) |
links |
code |
([\s\S]*?) |
multiline/code blocks |
`inline code` |
`([^`]+)` |
inline code |
- item |
^- (.*$) |
unordered list |
1. item |
^\d+\.(.*$) |
ordered list |
(Formatted and corrected by GPT4o - Tested personally and used it in this project.)
Add an arbitrary key for Local Storage in a file named config.js
:
export const CONFIG = {
STG_KEY: "Your_Key"
};
OR rename config.example.js
file to config.js
and put your key in Your_Key
.
(Or if you know the basics, use .env)
And in the end: Thank You! :D