This is a markdown example website. Source code of this article.
Paragraphs #
Markdown is a lightweight markup language with concise syntax, allowing people to focus more on the content itself rather than the layout. It uses a plain text format that is easy to read and write to write documents, can be mixed with HTML, and can export HTML, PDF, and its own .md format files. Because of its simplicity, efficiency, readability, and ease of writing, Markdown is widely used, such as Github, Wikipedia, Jianshu, etc.
Emphasis #
I just love bold text.
Italicized text is the cat's meow.
Underline requires <ins> HTML syntax
Delete this text.
Link to the github.
Quoting text #
Markdown is a lightweight markup language with concise syntax, allowing people to focus more on the content itself rather than the layout. It uses a plain text format that is easy to read and write to write documents, can be mixed with HTML, and can export HTML, PDF, and its own .md format files. Because of its simplicity, efficiency, readability, and ease of writing,
Markdownis widely used, such as Github, Wikipedia, Jianshu, etc.
Lists #
Ordered Lists
- First item
- Second item
- Third item
- Fourth item
Unordered Lists
- First item
- Second item
- Third item
- Fourth item
Task lists
Explorer #
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { isBinaryFileSync } from 'isbinaryfile';
const languageByExtension = {
c: 'c', cc: 'cpp', cpp: 'cpp', cxx: 'cpp', h: 'cpp', hpp: 'cpp',
css: 'css', html: 'html', htm: 'html', js: 'javascript', cjs: 'javascript',
mjs: 'javascript', json: 'json', jsx: 'jsx', lua: 'lua', md: 'markdown',
py: 'python', rb: 'ruby', rs: 'rust', sh: 'bash', bash: 'bash', sql: 'sql',
ts: 'typescript', tsx: 'tsx', txt: 'plaintext', xml: 'xml', yml: 'yaml', yaml: 'yaml'
};
const icons = {
folder: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M3 5.5A1.5 1.5 0 0 1 4.5 4H9l2 2h8.5A1.5 1.5 0 0 1 21 7.5v10a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 17.5v-12Z"/></svg>',
file: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M6 2h8l5 5v15H6V2Zm8 2.2V8h3.8L14 4.2ZM8 4v16h9V10h-5V4H8Z"/></svg>',
copy: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M9 8h11v13H9V8Zm2 2v9h7v-9h-7ZM4 3h11v3h-2V5H6v9h1v2H4V3Z"/></svg>'
};
function escapeHtml(value) {
return value.replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]);
}
function renderAssets() {
const stylesheet = fs.readFileSync(new URL('./style.css', import.meta.url), 'utf8');
const browserScript = fs.readFileSync(new URL('./browser.js', import.meta.url), 'utf8');
return `<style data-mdit-explorer-style>\n${stylesheet}</style><script data-mdit-explorer-script>\n${browserScript}</script>`;
}
function readDirectory(root, filters, current = root) {
const entries = fs.readdirSync(current, { withFileTypes: true })
.filter(entry => entry.name !== '.git' && !entry.isSymbolicLink())
.sort((a, b) => {
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
return a.name.localeCompare(b.name, 'en');
});
return entries.flatMap(entry => {
const absolutePath = path.join(current, entry.name);
const relativePath = path.relative(root, absolutePath).split(path.sep).join('/');
if (entry.isDirectory()) {
const children = readDirectory(root, filters, absolutePath);
if (filters.active && children.length === 0) return [];
return [{ type: 'directory', name: entry.name, path: relativePath, children }];
}
if (!entry.isFile()) return [];
if (filters.include.size > 0 && !filters.include.has(relativePath)) return [];
if (filters.exclude.has(relativePath)) return [];
const binary = isBinaryFileSync(absolutePath);
const content = binary ? 'Binary file' : fs.readFileSync(absolutePath, 'utf8');
return [{ type: 'file', name: entry.name, path: relativePath, content, binary }];
});
}
function flattenFiles(nodes) {
return nodes.flatMap(node => node.type === 'file' ? [node] : flattenFiles(node.children));
}
function renderTree(nodes, selectedPath) {
const items = nodes.map(node => {
if (node.type === 'directory') {
return `<li class="mexp__item"><div class="mexp__folder"><span class="mexp__icon">${icons.folder}</span><span class="mexp__name">${escapeHtml(node.name)}</span></div>${renderTree(node.children, selectedPath)}</li>`;
}
const extension = path.extname(node.name).slice(1).toLowerCase();
const selected = node.path === selectedPath;
return `<li class="mexp__item"><button class="mexp__file" type="button" role="treeitem" aria-selected="${selected}" data-path="${escapeHtml(node.path)}" data-name="${escapeHtml(node.name)}" data-ext="${escapeHtml(extension)}" title="${escapeHtml(node.path)}"><span class="mexp__icon">${icons.file}</span><span class="mexp__name">${escapeHtml(node.name)}</span></button></li>`;
}).join('');
return `<ul class="mexp__group" role="group">${items}</ul>`;
}
function highlightFile(md, file) {
const extension = path.extname(file.name).slice(1).toLowerCase();
const language = file.binary ? 'plaintext' : languageByExtension[extension] || extension;
const highlighted = !file.binary && typeof md.options.highlight === 'function'
? md.options.highlight(file.content, language, '')
: '';
return { html: highlighted || escapeHtml(file.content), language };
}
function isRemoteGitSource(source) {
try {
return ['http:', 'https:', 'file:'].includes(new URL(source).protocol);
} catch {
return false;
}
}
function sourceForLog(source) {
const url = new URL(source);
url.username = '';
url.password = '';
url.search = '';
url.hash = '';
return url.href;
}
function cloneRepository(source) {
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mdit-explorer-'));
const repositoryRoot = path.join(temporaryRoot, 'repository');
console.info(`[mdit-explorer] Cloning repository: ${sourceForLog(source)}`);
try {
execFileSync('git', ['clone', '--depth', '1', '--quiet', '--', source, repositoryRoot], {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 120000
});
} catch (error) {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
const detail = error.stderr?.toString().trim() || error.message;
throw new Error(`mdit-explorer: failed to clone repository: ${detail}`, { cause: error });
}
return { repositoryRoot, temporaryRoot };
}
function normalizeFilePath(filePath) {
return path.posix.normalize(filePath.replaceAll('\\', '/').replace(/^\.\/+/, ''));
}
function renderExplorerRoot(md, explorerRoot, directives, options) {
const include = new Set(directives.addFiles.map(normalizeFilePath));
const exclude = new Set(directives.excludeFiles.map(normalizeFilePath));
const tree = readDirectory(explorerRoot, { include, exclude, active: include.size > 0 || exclude.size > 0 });
const files = flattenFiles(tree);
if (files.length === 0) {
return '<div class="mexp mexp--empty" data-mdit-explorer>No files found.</div>';
}
const normalizedOpenPath = directives.defaultOpenPath
? normalizeFilePath(directives.defaultOpenPath)
: '';
const selected = files.find(file => file.path === normalizedOpenPath) || files[0];
const views = files.map(file => {
const hidden = file.path === selected.path ? '' : ' hidden';
const highlighted = highlightFile(md, file);
const languageClass = highlighted.language ? ` language-${escapeHtml(highlighted.language)}` : '';
const lineCount = options.lineNumbers === false
? ''
: ` data-line-count="${file.content.split('\n').length}"`;
return `<div class="mexp__view" data-path="${escapeHtml(file.path)}"${lineCount}${hidden}><pre class="mexp__code"><code class="hljs${languageClass}">${highlighted.html}</code></pre></div>`;
}).join('');
return `<div class="mexp" data-mdit-explorer><aside class="mexp__sidebar"><div class="mexp__side-title">Explorer</div><nav class="mexp__tree" aria-label="File explorer">${renderTree(tree, selected.path)}</nav></aside><section class="mexp__main"><div class="mexp__tabbar"><div class="mexp__tab"><span class="mexp__tab-name">${escapeHtml(selected.name)}</span></div></div><div class="mexp__crumbbar"><span class="mexp__crumb">${escapeHtml(selected.path)}</span><div class="mexp__actions"><button class="mexp__copy" type="button" data-path="${escapeHtml(selected.path)}" aria-label="Copy code">${icons.copy}</button></div></div><div class="mexp__views">${views}</div></section></div>`;
}
function renderExplorer(md, directives, options) {
const { source } = directives;
if (isRemoteGitSource(source)) {
const { repositoryRoot, temporaryRoot } = cloneRepository(source);
try {
console.info(`[mdit-explorer] Parsing repository: ${sourceForLog(source)}`);
return renderExplorerRoot(md, repositoryRoot, directives, options);
} finally {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
}
const baseDir = path.resolve(options.root || process.cwd());
const explorerRoot = path.resolve(baseDir, source);
const relativeToBase = path.relative(baseDir, explorerRoot);
if (relativeToBase.startsWith('..') || path.isAbsolute(relativeToBase)) {
throw new Error(`mdit-explorer: directory is outside root: ${source}`);
}
if (!fs.statSync(explorerRoot).isDirectory()) {
throw new Error(`mdit-explorer: not a directory: ${source}`);
}
return renderExplorerRoot(md, explorerRoot, directives, options);
}
function parseInfo(info) {
const match = info.trim().match(/^explorer(?:\s+(.+))?$/);
return match?.[1]?.trim() || null;
}
export default function explorerPlugin(md, options = {}) {
md.block.ruler.before('fence', 'explorer', (state, startLine, endLine, silent) => {
const start = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
const marker = state.src.slice(start, max);
if (!marker.startsWith(':::')) return false;
const source = parseInfo(marker.slice(3));
if (!source) return false;
let nextLine = startLine + 1;
let defaultOpenPath = '';
const addFiles = [];
const excludeFiles = [];
for (; nextLine < endLine; nextLine += 1) {
const lineStart = state.bMarks[nextLine] + state.tShift[nextLine];
const line = state.src.slice(lineStart, state.eMarks[nextLine]).trim();
if (line === ':::') break;
if (line.startsWith('defaultopen=')) defaultOpenPath = line.slice('defaultopen='.length).trim();
if (line.startsWith('addfile=')) {
const filePath = line.slice('addfile='.length).trim();
if (filePath) addFiles.push(filePath);
}
if (line.startsWith('excludefile=')) {
const filePath = line.slice('excludefile='.length).trim();
if (filePath) excludeFiles.push(filePath);
}
}
if (nextLine >= endLine) return false;
if (silent) return true;
const token = state.push('explorer', '', 0);
token.block = true;
token.map = [startLine, nextLine + 1];
token.meta = { source, defaultOpenPath, addFiles, excludeFiles };
state.line = nextLine + 1;
return true;
});
md.renderer.rules.explorer = (tokens, index) => {
const token = tokens[index];
const firstExplorer = tokens.findIndex(item => item.type === 'explorer') === index;
const assets = firstExplorer && options.injectAssets === true ? renderAssets() : '';
return `${assets}${renderExplorer(md, token.meta, options)}\n`;
};
}
{
"name": "mdit-explorer",
"version": "0.3.0",
"description": "A markdown-it plugin, For rendering an embedded code explorer similar to VS Code style.",
"main": "./src/index.js",
"author": "fdxx",
"license": "GPLv3",
"type": "module",
"homepage": "https://github.com/fdxx/mdit-explorer",
"repository": {
"type": "git",
"url": "https://github.com/fdxx/mdit-explorer.git"
},
"engines": {
"node": ">=24"
},
"exports": {
".": "./src/index.js",
"./style.css": "./src/style.css",
"./browser.js": "./src/browser.js"
},
"files": [
"src",
"README.md"
],
"scripts": {
"demo": "node scripts/build-demo.js",
"test": "node --test"
},
"keywords": [
"markdown-it",
"code-explorer",
"markdown"
],
"peerDependencies": {
"markdown-it": ">=14"
},
"devDependencies": {
"highlight.js": "^11.10.0",
"markdown-it": ">=14"
},
"dependencies": {
"isbinaryfile": "^6.0.0"
}
}
# mdit-explorer
A markdown-it plugin, For rendering an embedded code explorer similar to VS Code style.
## In the md file
```md
::: explorer ./demo
defaultopen=src/main.cpp
addfile=src/main.cpp
addfile=src/main.h
excludefile=src/main.h
:::
```
- `./demo`: The folder to be parsed and scanned.
- `defaultopen`: The file to open by default.
- `addfile`: Optional. Only render the specified file. Repeat the directive to add multiple files.
- `excludefile`: Optional. Exclude the specified file. Repeat the directive to exclude multiple files. Exclusions take precedence over additions.
Remote HTTP(S) Git repositories are shallow-cloned while Markdown is rendered:
```md
::: explorer https://github.com/fdxx/mdit-explorer.git
defaultopen=src/index.js
:::
```
The generated HTML contains the repository files and does not access Git at runtime. Binary files remain visible in the tree and display `Binary file` instead of their contents.
## Use in markdown-it
```js
import markdownit from 'markdown-it';
import hljs from 'highlight.js';
import explorer from 'mdit-explorer';
const mdrenderer = markdownit({ html: true, highlight })
.use(explorer)
```
## Plugin Options
```js
import explorer from 'mdit-explorer';
const mdrenderer = markdownit({ html: true, highlight })
.use(explorer, { injectAssets: false, lineNumbers: true, root: 'path/to' })
```
- `injectAssets`: Inline `style.css` and `browser.js`. They are injected only once when rendering multiple blocks. Defaults to `false`.
- `lineNumbers`: Show line numbers in the code area. Defaults to `true`; `browser.js` generates them only when a file is viewed for the first time.
- `root`: Where to start searching the directory. Defaults to `process.cwd()`.
Table #
| Left-aligned | Center-aligned | Right-aligned |
|---|---|---|
| git status | git status | git status |
| git diff | git diff | git diff |
Code blocks #
#include <iostream>
int main(int argc, char *argv[]) {
/* An annoying "Hello World" example */
for (auto i = 0; i < 0xFFFF; i++)
cout << "Hello, World!" << endl;
char c = '\n';
unordered_map <string, vector<string> > m;
m["key"] = "\\\\"; // this is an error
return -2e3 + 12l;
}
package main
import "fmt"
func main() {
ch := make(chan float64)
ch <- 1.0e10 // magic number
x, ok := <- ch
defer fmt.Println(`exitting now\`)
go println(len("hello world!"))
return
}
#!/bin/bash
###### CONFIG
ACCEPTED_HOSTS="/root/.hag_accepted.conf"
BE_VERBOSE=false
if [ "$UID" -ne 0 ]
then
echo "Superuser rights required"
exit 2
fi
genApacheConf(){
echo -e "# Host ${HOME_DIR}$1/$2 :"
}
echo '"quoted"' | tr -d \" > text.txt
[
{
"title": "apples",
"count": [12000, 20000],
"description": {"text": "...", "sensitive": false}
},
{
"title": "oranges",
"count": [17500, null],
"description": {"text": "...", "sensitive": false}
}
]
Footnotes #
Here is a simple footnote[1].
The footnote will appear at the bottom[2].
Collapsed sections #
Click to expand
You can temporarily obscure sections of your Markdown by creating a collapsed section that the reader can choose to expand. For example, when you want to include technical details in an issue comment that may not be relevant or interesting to every reader, you can put those details in a collapsed section.Emoji #
😂🤣😡📱🇨🇳😍
Tabs #
This is Apple!
This is Banana!
This is Orange!
aaa
bbb
ccc
Alerts #
Note
Useful information that users should know, even when skimming content.
Tip
Helpful advice for doing things better or more easily.
Important
Key information users need to know to achieve their goal.
Warning
Urgent info that needs immediate user attention to avoid problems.
Caution
Advises about risks or negative outcomes of certain actions.
Images #
Online images
Local images
![]()