Skip to main content

Syntax Highlighting

Markdown-OS provides comprehensive syntax highlighting for code blocks using highlight.js. The editor supports 190+ programming languages with automatic language detection, line numbers, copy functionality, and theme-synchronized color schemes.

Creating Code Blocks

Code blocks are created using fenced code blocks with an optional language identifier:
```javascript
function hello(name) {
  console.log(`Hello, ${name}!`);
}
```
The language identifier (e.g., javascript) is used for syntax highlighting. If omitted, the code is displayed as plain text.

Supported Languages

highlight.js supports a wide range of languages including:
  • JavaScript/TypeScript: javascript, typescript, jsx, tsx
  • Python: python, py
  • Java/Kotlin: java, kotlin
  • C/C++/C#: c, cpp, csharp, cs
  • Go: go, golang
  • Rust: rust, rs
  • Ruby: ruby, rb
  • PHP: php
  • Swift: swift

Web Technologies

  • HTML/XML: html, xml
  • CSS/SCSS: css, scss, sass, less
  • SQL: sql, mysql, postgresql
  • GraphQL: graphql, gql
  • JSON: json
  • YAML: yaml, yml

Shell & Config

  • Bash/Shell: bash, sh, shell
  • PowerShell: powershell, ps1
  • Dockerfile: dockerfile
  • Nginx: nginx
  • Apache: apache

Markup & Data

  • Markdown: markdown, md
  • LaTeX: latex, tex
  • TOML: toml
  • INI: ini
For the complete list of supported languages, see the highlight.js documentation.

Code Block Features

Every code block is enhanced with interactive features:

Visual Components

1

Language Label

Top-left corner shows the programming language (e.g., “javascript”, “python”)
2

Line Numbers

Left gutter displays line numbers for easy reference
3

Copy Button

Top-right copy icon copies the entire code block to clipboard
4

Edit Button

Edit icon opens a modal to modify the code and change the language

Implementation

function buildCodeBlock(codeElement) {
  const wrapper = document.createElement("div");
  wrapper.className = "code-block";
  wrapper.setAttribute("contenteditable", "false");
  
  const codeSource = codeElement.textContent || "";
  wrapper.dataset.rawSource = codeSource;
  wrapper.dataset.language = languageLabel;
  
  // Create header with language label and actions
  const header = document.createElement("div");
  header.className = "code-block-header";
  
  const label = document.createElement("span");
  label.className = "code-language-label";
  label.textContent = languageLabel;
  
  const actions = document.createElement("div");
  actions.className = "code-block-actions";
  
  // Copy button
  const copyButton = createActionButton("copy", "Copy code");
  copyButton.addEventListener("click", async (event) => {
    await copyToClipboard(wrapper.dataset.rawSource || "");
    flashCopied(copyButton);
  });
  
  // Edit button
  const editButton = createActionButton("edit", "Edit code block");
  editButton.addEventListener("click", (event) => {
    openBlockEditor("code", wrapper);
  });
  
  // Apply syntax highlighting
  if (window.hljs && !codeElement.classList.contains("hljs")) {
    window.hljs.highlightElement(codeElement);
  }
}

Line Numbers

Line numbers are automatically generated based on the code content:
function countCodeLines(content) {
  if (!content) {
    return 1;
  }
  return Math.max(1, content.split("\n").length);
}

function createLineNumberGutter(lineCount) {
  const gutter = document.createElement("div");
  gutter.className = "code-line-numbers";
  gutter.setAttribute("aria-hidden", "true");
  
  for (let line = 1; line <= lineCount; line += 1) {
    const lineNumber = document.createElement("span");
    lineNumber.className = "code-line-number";
    lineNumber.textContent = String(line);
    gutter.appendChild(lineNumber);
  }
  
  return gutter;
}
Line numbers are marked with aria-hidden="true" to prevent screen readers from announcing them.

Copy Functionality

The copy button provides visual feedback when code is copied:
async function copyToClipboard(text) {
  if (navigator.clipboard && navigator.clipboard.writeText) {
    await navigator.clipboard.writeText(text);
    return;
  }
  
  // Fallback for older browsers
  const fallbackInput = document.createElement("textarea");
  fallbackInput.value = text;
  fallbackInput.setAttribute("readonly", "true");
  fallbackInput.style.position = "absolute";
  fallbackInput.style.left = "-9999px";
  document.body.appendChild(fallbackInput);
  fallbackInput.select();
  document.execCommand("copy");
  document.body.removeChild(fallbackInput);
}

Copy Feedback

function flashCopied(button) {
  const originalIcon = button.innerHTML;
  const originalTitle = button.getAttribute("title");
  
  // Clear any existing timer
  const previousTimerId = button.dataset.copyTimerId;
  if (previousTimerId) {
    window.clearTimeout(Number.parseInt(previousTimerId, 10));
  }
  
  // Show checkmark
  button.innerHTML = actionIconSvg("check");
  button.setAttribute("title", "Copied");
  button.classList.add("copied");
  
  // Restore after 1.5 seconds
  const timerId = window.setTimeout(() => {
    button.classList.remove("copied");
    button.innerHTML = originalIcon;
    button.setAttribute("title", originalTitle);
    delete button.dataset.copyTimerId;
  }, 1500);
  
  button.dataset.copyTimerId = String(timerId);
}
The copy button temporarily changes to a checkmark for 1.5 seconds after successful copy, providing clear visual feedback.

Editing Code Blocks

Click the edit button to modify code:
The block editor opens with:
  • Title: “Edit code block”
  • Source textarea: Pre-filled with current code
  • Language input: Current language identifier (e.g., “javascript”)
  • Save/Cancel buttons: Keyboard shortcuts supported
After saving:
  1. Source and language are stored in data attributes
  2. New line numbers are generated based on updated content
  3. highlight.js re-highlights the code with the new language
  4. Change event is emitted for auto-save
async function applyBlockEdit() {
  const sourceInput = document.getElementById("block-edit-source");
  const languageInput = document.getElementById("block-edit-language");
  const source = sourceInput.value;
  
  if (state.blockEditType === "code") {
    const language = languageInput?.value?.trim() || "text";
    state.blockEditTarget.dataset.rawSource = source;
    state.blockEditTarget.dataset.language = language;
    
    // Rebuild code element
    const pre = document.createElement("pre");
    const code = document.createElement("code");
    code.className = `language-${language}`;
    code.textContent = source;
    pre.appendChild(code);
    
    // Update content
    const content = state.blockEditTarget.querySelector(".code-block-content");
    content.innerHTML = "";
    content.appendChild(createLineNumberGutter(countCodeLines(source)));
    content.appendChild(pre);
    
    // Update language label
    const label = state.blockEditTarget.querySelector(".code-language-label");
    label.textContent = language;
    
    // Re-highlight
    if (window.hljs) {
      window.hljs.highlightElement(code);
    }
    
    closeBlockEditor();
    emitChange();
  }
}

Theme Integration

highlight.js color schemes synchronize with the active editor theme:
const THEMES = [
  { id: "light", highlightTheme: "github" },
  { id: "dark", highlightTheme: "github-dark" },
  { id: "dracula", highlightTheme: "base16/dracula" },
  { id: "nord-light", highlightTheme: "github" },
  { id: "nord-dark", highlightTheme: "nord" },
  { id: "lofi", highlightTheme: "grayscale" },
];

Dynamic Theme Loading

function updateHighlightTheme(highlightThemeId) {
  if (!this.highlightTheme) {
    return;
  }
  
  const highlightHref = `${HIGHLIGHT_THEME_BASE}/${highlightThemeId}.min.css`;
  if (this.highlightTheme.getAttribute("href") === highlightHref) {
    return;
  }
  
  this.highlightTheme.setAttribute("href", highlightHref);
}
Theme stylesheets are loaded from the highlight.js CDN dynamically when the theme changes.

Language Detection

If no language is specified, the code is labeled as “text”:
function inferLanguageLabel(codeElement) {
  const languageClass = Array.from(codeElement.classList).find((className) =>
    className.startsWith("language-"),
  );
  if (!languageClass) {
    return "text";
  }
  return languageClass.replace("language-", "");
}
Code blocks without a language identifier are not syntax-highlighted but still get line numbers, copy buttons, and edit functionality.

Markdown Serialization

When saving, code blocks are converted back to fenced code blocks:
turndownService.addRule("fencedCode", {
  filter(node) {
    return node.nodeType === Node.ELEMENT_NODE && node.nodeName === "PRE";
  },
  replacement(_content, node) {
    const codeNode = node.firstElementChild;
    if (!codeNode || codeNode.nodeName !== "CODE") {
      return "\n\n```\n```\n\n";
    }
    
    const codeText = codeNode.textContent || "";
    const languageClass = Array.from(codeNode.classList).find((className) =>
      className.startsWith("language-"),
    );
    const language = languageClass
      ? languageClass.replace("language-", "")
      : "";
    
    return `\n\n\`\`\`${language}\n${codeText}\n\`\`\`\n\n`;
  },
});
The serialization uses the raw source from data-raw-source attributes to preserve the original code exactly.

Code Block Structure

Each code block has a specific DOM structure:
<div class="code-block" contenteditable="false" data-raw-source="..." data-language="javascript">
  <div class="code-block-header">
    <span class="code-language-label">javascript</span>
    <div class="code-block-actions">
      <button class="action-icon-button block-edit-trigger" title="Edit code block">...</button>
      <button class="action-icon-button copy-button" title="Copy code">...</button>
    </div>
  </div>
  <div class="code-block-content">
    <div class="code-line-numbers" aria-hidden="true">
      <span class="code-line-number">1</span>
      <span class="code-line-number">2</span>
      <!-- ... -->
    </div>
    <pre><code class="language-javascript hljs"><!-- highlighted code --></code></pre>
  </div>
</div>
The wrapper has contenteditable="false" to prevent accidental editing, requiring users to use the edit button instead.

Cleanup on Serialization

Before converting to markdown, UI elements are removed:
function cleanupForSerialization(cloneRoot) {
  // Remove buttons and controls
  cloneRoot.querySelectorAll(
    ".copy-button, .block-edit-trigger, .code-line-numbers"
  ).forEach((node) => {
    node.remove();
  });
  
  // Restore original code structure
  cloneRoot.querySelectorAll(".code-block").forEach((wrapper) => {
    const source = wrapper.dataset.rawSource || wrapper.querySelector("pre code")?.textContent;
    const language = wrapper.dataset.language || "";
    const pre = document.createElement("pre");
    const code = document.createElement("code");
    if (language) {
      code.className = `language-${language}`;
    }
    code.textContent = source;
    pre.appendChild(code);
    wrapper.replaceWith(pre);
  });
}

Performance Considerations

Large code blocks: highlight.js handles code blocks with thousands of lines efficiently, but rendering many large blocks at once may cause brief delays.
Lazy highlighting: Consider splitting very large files into multiple smaller code blocks for better performance.

Best Practices

Always specify the language: This enables proper syntax highlighting and helps readers understand the code context.
Avoid extremely long lines: Lines over 200 characters may cause horizontal scrolling. Consider breaking them for better readability.
Language aliases: Many languages have multiple valid identifiers (e.g., js = javascript, py = python). Use the most common form for consistency.

Build docs developers (and LLMs) love