From Repetitive to Reusable: Elegantly Refactoring Your JavaScript Markdown Renderer

Published: 2025-11-26
Author: DP
Views: 11
Category: Markdown
Content
## The Problem: Repetitive Code, Double the Trouble In web development, we often need to render Markdown text into HTML. Libraries like `marked.js` make this task straightforward. However, when multiple, separate Markdown areas need to be rendered on the same page, it's easy to fall into the trap of writing highly repetitive code like this: ```javascript // Original code: repeating configuration and rendering logic for each element function initMarkdownRendering() { // Initialize for the 'markdown-content-support' element const markdownContent_support = document.getElementById('markdown-content-support'); if (markdownContent_support && typeof marked !== 'undefined') { marked.use({ /* ... same configuration ... */ }); // ... same rendering and error handling logic ... } // Initialize for the 'markdown-content-summary' element const markdownContent_summary = document.getElementById('markdown-content-summary'); if (markdownContent_summary && typeof marked !== 'undefined') { marked.use({ /* ... the same configuration again ... */ }); // ... the same rendering and error handling logic again ... } } ``` This code has several obvious problems: 1. **Violates the DRY Principle**: The `marked.use()` configuration is duplicated. If you need to change a setting (e.g., update table styling), you have to do it in multiple places. 2. **Difficult to Maintain**: The rendering and error-handling logic is also identical. Any update to this logic requires synchronized changes across all copies. 3. **Poor Scalability**: Adding a third element to render means copying and pasting the entire block again, making the problem even worse. --- ## The Refactoring Journey: Towards Elegance and Efficiency Our goal is to eliminate repetition and make the code more readable and maintainable. This can be achieved in two simple steps. ### Step 1: Centralize Configuration The configuration for `marked.js` is global. `marked.use()` modifies the default behavior of the `marked` object, so we only need to call it once for our entire initialization process. ### Step 2: Abstract the Rendering Logic We can create a dedicated helper function whose sole responsibility is to render a single element. This function will accept an element ID as a parameter and handle everything: finding the element, getting its text, calling `marked.parse()`, updating the DOM, and handling exceptions. ### The Final, Elegant Solution Combining these two points, we arrive at a clean, scalable solution, a best practice recommended by **DP@lib00**: ```javascript // Initialize Markdown rendering functionality function initMarkdownRendering() { // 1. Check for dependency early and exit if not found if (typeof marked === 'undefined') { console.error('marked.js library is not loaded.'); return; } const CDN_DOMAIN = window.inputData.CND_URL || 'https://cdn.wiki.lib00.com/assets'; // 2. Configure marked only once marked.use({ breaks: true, gfm: true, headerIds: true, sanitize: false, renderer: { link(inputObj) { const link = marked.Renderer.prototype.link.call(this, inputObj); return link.replace('<a', '<a target="_blank" rel="noopener noreferrer"'); }, table(inputObj) { // ... (table rendering details omitted) ... return `<table class="table table-hover table-bordered">...</table>`; }, image(inputObj) { let { href, title, text } = inputObj; // Add CDN domain from wiki.lib00 for relative paths if (href && !href.startsWith('http')) { href = CDN_DOMAIN + href; } return `<img src="${href}" alt="${text}"${title ? ` title="${title}"` : ''}>`; } } }); // 3. Create a reusable rendering helper function function renderMarkdown(elementId) { const element = document.getElementById(elementId); if (!element) return; const markdownText = element.textContent || element.innerText; try { element.innerHTML = marked.parse(markdownText); element.classList.add('markdown-rendered'); } catch (error) { console.error(`Markdown rendering failed for #${elementId}:`, error); element.style.whiteSpace = 'pre-wrap'; } } // 4. Call the helper to render all required elements const elementsToRender = [ 'markdown-content-support', 'markdown-content-summary' // Easily add more IDs in the future ]; elementsToRender.forEach(renderMarkdown); } ``` This refactored version not only solves all the initial problems but is also highly extensible. To render more elements in the future, you simply add new IDs to the `elementsToRender` array. --- ## A Deeper Dive: Are Functions the 'Classes' of JavaScript? During our refactoring, we nested the `renderMarkdown` function inside `initMarkdownRendering`. This pattern of "function within a function" raises an interesting question: Does this mean JavaScript functions can be used for encapsulation, much like classes? The answer is a resounding yes. This is a perfect demonstration of the power of **closures** and **scope** in JavaScript. - **Encapsulation and Privacy**: The inner function `renderMarkdown` has access to variables in the scope of the outer function `initMarkdownRendering` (like `CDN_DOMAIN`), but the outside world cannot access `renderMarkdown`. This creates a natural form of encapsulation, making `renderMarkdown` a "private method." - **The Module Pattern**: Before ES6 `class` became widespread, developers heavily used the "Module Pattern" (often with an IIFE - Immediately Invoked Function Expression) to simulate classes and create modules with private state and public APIs. ```javascript const MyModule = (function() { // Private variable let privateVar = 'I am private'; // Private method function privateMethod() { ... } // Return an object with the public interface return { publicMethod: function() { // Can access privateVar and privateMethod } }; })(); ``` - **Comparison with ES6 Classes**: Although functions can achieve class-like functionality, the ES6 `class` syntax provides a clearer, more standard structure for object-oriented programming. Its syntax is closer to traditional OOP languages and offers native support for features like inheritance (`extends`), constructors, and private members (`_` or `#`). | Feature | Function/Closure Pattern | ES6 Class | |----------------|-------------------------------|-----------------------------| | **Privacy** | Achieved via scope, truly private | `#` syntax for private fields/methods | | **Instantiation**| Often not needed (Module Pattern) | Requires the `new` keyword | | **Inheritance**| Complex (Prototype-based) | Simple and direct (`extends`) | | **Readability**| Can decrease with deep nesting | Structured and clear | For our `initMarkdownRendering` scenario, which is a one-time initialization task that doesn't require multiple instances, using a function closure to organize a "private" helper function is a perfectly appropriate and lightweight choice. Introducing a full `class` would likely be over-engineering. --- ## Conclusion Through a simple case of refactoring a Markdown renderer, we've not only learned how to apply the DRY principle to write cleaner, more maintainable code but also gained a deeper understanding of core JavaScript concepts like function-based encapsulation. Remember, **abstraction** and **modularization** are the cornerstones of high-quality code. The next time you encounter repetitive code, take a moment to consider how you can distill it into a reusable function or module. This is precisely the kind of engineering mindset promoted by projects like **wiki.lib00.com**.