作为一名经常写文档的开发者,你是否遇到过这样的场景:写好了一篇 Markdown 笔记,却需要把它转成 HTML 放到网页上?每次都打开在线转换工具太麻烦,用 Pandoc 又太重。
今天这篇文章,我将用 OpenCode 在 3 分钟内写一个命令行 Markdown 转 HTML 工具。核心技术栈:Python 3 + 标准库(零依赖),最终代码约 110 行,支持标题、段落、链接、图片、代码块、列表和水平分割线。
我先让 OpenCode 帮我把最核心的转换逻辑写出来——处理标题和段落。
我对 OpenCode 说:
> 帮我写一个 Python 脚本,把 Markdown 文本转成 HTML。先实现标题(# 到 ######)和段落的转换。不需要依赖任何第三方库。
OpenCode 立刻生成了一份代码,核心函数 convert_md_to_html() 逐行扫描 Markdown 文本,用正则匹配 # 开头的行作为标题,其余非空行归为段落。
关键要点:这里我只描述了"做什么",没有限定"怎么做"。OpenCode 自己选择了逐行扫描 + 正则匹配的方案,这正是 AI 编程的核心价值——你不需要知道实现细节,只需要清晰表达需求。
骨架有了,但一个实用的转换器至少得支持链接、图片和内联代码。
我对 OpenCode 说:
> 很好!现在加上对内联元素的支持:粗体(text)、斜体(*text*)、行内代码(code)、链接(text)和图片(!alt)。
OpenCode 新增了一个 parse_inline() 函数,用正则表达式依次替换这些 Markdown 内联语法。
关键要点:注意我的提示词是对话式的——"很好!现在加上……"。这很像你和同事结对编程时的交流方式,OpenCode 能理解上下文,在已有代码基础上增量修改。
光有转换函数不够,工具得能从文件读取、向文件输出、处理管道。
我对 OpenCode 说:
> 加上命令行参数解析,支持:md2html input.md 从文件读取,-o output.html 指定输出文件,-w 包裹完整 HTML 文档结构(带基本 CSS 样式),同时支持从 stdin 读取。
这一步 OpenCode 引入 argparse,添加了 --wrap 选项,并在没有输入文件时自动从标准输入读取——这样就能配合管道使用了。
关键要点:OpenCode 自动处理了边界情况:文件编码、管道输入、条件输出。你不会漏掉 encoding='utf-8' 或 if args.output 这种细节。
代码块和列表是 Markdown 的重要组成,缺少它们就不算完整工具。
我对 OpenCode 说:
> 补充支持:围栏代码块(``language ... ``)、无序列表(- * + 开头)、水平分割线(--- *** ___)。
OpenCode 在循环中加入了 in_code_block 状态标记来处理多行代码块,列表项则以 <li> 包裹输出。
关键要点:代码块是最复杂的部分,因为它跨越了多行。我原本以为需要自己描述状态机逻辑,但 OpenCode 自动就用了 in_code_block 标志位——这说明它对这类常见编程模式有很好的"语感"。
最后看看有没有遗漏。
我对 OpenCode 说:
> 处理粗斜体组合(*text*),另外代码块未闭合时自动补上闭合标签。
OpenCode 修复了粗体+斜体的正则替换顺序(先匹配 * 再匹配 和 *,防止被错误截断),并在函数末尾添加了未闭合标签的检查。
整个开发过程就是五句话,OpenCode 每步都在 5-10 秒内完成代码生成。
#!/usr/bin/env python3
"""Markdown to HTML converter - zero-dependency CLI tool."""
import re
import sys
import argparse
from pathlib import Path
def parse_inline(text: str) -> str:
"""Convert inline Markdown to HTML."""
text = re.sub(r'\*\*\*(.+?)\*\*\*', r'<strong><em>\1</em></strong>', text)
text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
text = re.sub(r'\*(.+?)\*', r'<em>\1</em>', text)
text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text)
text = re.sub(r'!\[([^\]]*)\]\(([^)]+)\)', r'<img src="\2" alt="\1">', text)
text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<a href="\2">\1</a>', text)
return text
def convert_md_to_html(md_text: str) -> str:
"""Convert Markdown text to HTML."""
lines = md_text.split('\n')
result = []
in_code_block = False
i = 0
while i < len(lines):
line = lines[i]
if line.startswith('```'):
if in_code_block:
result.append('</code></pre>')
in_code_block = False
else:
lang = line[3:].strip()
cls = f' class="language-{lang}"' if lang else ''
result.append(f'<pre><code{cls}>')
in_code_block = True
i += 1
continue
if in_code_block:
result.append(line)
i += 1
continue
heading = re.match(r'^(#{1,6})\s+(.+)$', line)
if heading:
level = len(heading.group(1))
content = parse_inline(heading.group(2))
result.append(f'<h{level}>{content}</h{level}>')
i += 1
continue
if re.match(r'^(\*{3,}|-{3,}|_{3,})$', line.strip()):
result.append('<hr>')
i += 1
continue
list_item = re.match(r'^(\s*)[-*+]\s+(.+)$', line)
if list_item:
result.append('<ul>')
while i < len(lines):
m = re.match(r'^(\s*)[-*+]\s+(.+)$', lines[i])
if not m:
break
result.append(f' <li>{parse_inline(m.group(2))}</li>')
i += 1
result.append('</ul>')
continue
if line.strip() == '':
i += 1
continue
result.append('<p>')
para_parts = []
while i < len(lines) and lines[i].strip() and \
not re.match(r'^(#{1,6})\s+', lines[i]) and \
not re.match(r'^(\s*)[-*+]\s+', lines[i]) and \
not re.match(r'^(\*{3,}|-{3,}|_{3,})$', lines[i].strip()) and \
not lines[i].startswith('```'):
para_parts.append(parse_inline(lines[i]))
i += 1
result.append('<br>\n'.join(para_parts))
result.append('</p>')
if in_code_block:
result.append('</code></pre>')
return '\n'.join(result)
def main():
parser = argparse.ArgumentParser(
description='Convert Markdown to HTML',
usage='%(prog)s input.md [-o output.html] [-w]'
)
parser.add_argument('input', nargs='?', help='Input Markdown file')
parser.add_argument('-o', '--output', help='Output HTML file')
parser.add_argument('-w', '--wrap', action='store_true',
help='Wrap with full HTML document structure')
args = parser.parse_args()
if args.input:
md_text = Path(args.input).read_text(encoding='utf-8')
else:
md_text = sys.stdin.read()
body = convert_md_to_html(md_text)
if args.wrap:
result = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body{{font-family:-apple-system,BlinkMacSystemFont,sans-serif;
max-width:800px;margin:0 auto;padding:2rem;line-height:1.6;color:#333}}
code{{background:#f4f4f4;padding:2px 6px;border-radius:3px}}
pre{{background:#f4f4f4;padding:1rem;border-radius:5px;overflow-x:auto}}
pre code{{background:none;padding:0}}
img{{max-width:100%}}
hr{{border:none;border-top:1px solid #ddd;margin:2rem 0}}
</style>
</head>
<body>
{body}
</body>
</html>"""
else:
result = body
if args.output:
Path(args.output).write_text(result, encoding='utf-8')
print(f'✓ Saved to {args.output}')
else:
print(result)
if __name__ == '__main__':
main()
使用方式:
# 文件转文件 python md2html.py readme.md -o readme.html -w # 管道输入 echo "# Hello **World**" | python md2html.py # 结合剪切板(macOS) pbpaste | python md2html.py -w > output.html
整个工具从零到可用的完整代码,我只说了 5 句话,耗时不到 3 分钟。对比传统开发方式,省去了查阅文档、手动敲代码、调试语法的时间。
OpenCode 的核心优势不在它能写出多复杂的代码,而在于你只需要考虑"要什么",不用纠结"怎么写"。这种体验在做这些小工具时尤其明显——想法冒出来,话说完,代码就有了。
对于日常开发中的脚本、工具、原型验证,OpenCode 已经可以替代至少 60% 的手写工作。剩下的 40% 是架构决策和业务逻辑设计——这些恰恰是 AI 暂时无法替代的人类判断力。