項目目錄/
├── index.html # 內容頁面
├── chapter1/
│ ├── section1.html
│ └── section2.html
├── images/
│ └── logo.png
└── 生成文件/
├── project.hhp # 項目配置
├── project.hhc # 目錄結構 (TOC)
└── project.hhk # 關鍵字索引
<?php
declare(strict_types=1);
class ChmBuilder
{
private string $srcDir;
private string $outDir;
private string $chmName;
private string $title;
private string $firstPage;
private array $excludeDirs = ['.git', '.svn', 'node_modules', 'vendor'];
private array $excludeFiles = ['.DS_Store', 'Thumbs.db'];
private array $imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg'];
public function __construct(
string $srcDir,
string $outDir = '',
string $chmName = 'manual',
string $title = 'Documentation',
string $firstPage = 'index.html'
) {
$this->srcDir = rtrim($srcDir, '/\\');
$this->outDir = $outDir ?: $this->srcDir;
$this->chmName = $chmName;
$this->title = $title;
$this->firstPage = $firstPage;
}
public function build(): bool
{
$this->buildHhp();
$this->buildHhc();
$this->buildHhk();
return $this->compile();
}
/** 1. 構建 .hhp 項目文件 */
private function buildHhp(): void
{
$files = $this->scanFiles($this->srcDir);
$fileList = implode("\n", $files);
$content = <<outDir}/{$this->chmName}.chm
Contents file={$this->chmName}.hhc
Index file={$this->chmName}.hhk
Default topic={$this->firstPage}
Display compile progress=No
Full-text search=Yes
Language=0x804
Title={$this->title}
ImageType=Folder
[FILES]
{$fileList}
EOF;
file_put_contents("{$this->outDir}/{$this->chmName}.hhp", $content);
}
/** 2. 構建 .hhc 目錄結構 */
private function buildHhc(): void
{
$tree = $this->buildDirTree($this->srcDir);
$tocItems = $this->renderToc($tree);
$content = <<
{$tocItems}
EOF;
file_put_contents("{$this->outDir}/{$this->chmName}.hhc", $content);
}
/** 3. 構建 .hhk 關鍵字索引 */
private function buildHhk(): void
{
$files = $this->scanFiles($this->srcDir);
$indexItems = [];
foreach ($files as $file) {
if (preg_match('/\.html?$/i', $file)) {
$title = basename($file, '.html');
$title = basename($title, '.htm');
$indexItems[] = <<-
ITEM;
}
}
$content = <<
EOF;
$content .= implode("\n", $indexItems);
$content .= "\n
";
file_put_contents("{$this->outDir}/{$this->chmName}.hhk", $content);
}
/** 調用 hhc.exe 編譯 */
private function compile(): bool
{
$hhpPath = "{$this->outDir}/{$this->chmName}.hhp";
if (!file_exists($hhpPath)) return false;
// Windows 環境下 hhc.exe 通常在 "C:\Program Files (x86)\HTML Help Workshop\hhc.exe"
$hhcExe = $this->findHhcExe();
if (!$hhcExe) {
trigger_error('未找到 hhc.exe,請安裝 HTML Help Workshop', E_USER_WARNING);
return false;
}
$cmd = '"' . $hhcExe . '" "' . $hhpPath . '"';
$output = [];
$returnVar = 0;
exec($cmd, $output, $returnVar);
return $returnVar === 0 && file_exists("{$this->outDir}/{$this->chmName}.chm");
}
private function findHhcExe(): ?string
{
$paths = [
'C:\Program Files (x86)\HTML Help Workshop\hhc.exe',
'C:\Program Files\HTML Help Workshop\hhc.exe',
getenv('ProgramFiles(x86)') . '\HTML Help Workshop\hhc.exe',
];
foreach ($paths as $p) {
if (file_exists($p)) return $p;
}
return null;
}
/** 掃描所有 HTML/圖片文件 */
private function scanFiles(string $dir): array
{
$files = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$relPath = str_replace('\\', '/', substr($file->getPathname(), strlen($this->srcDir) + 1));
$ext = strtolower($file->getExtension());
if ($ext === 'html' || $ext === 'htm' || in_array($ext, $this->imageTypes)) {
if (!$this->isExcluded($relPath)) {
$files[] = $relPath;
}
}
}
}
sort($files, SORT_NATURAL | SORT_FLAG_CASE);
return $files;
}
/** 構建目錄樹 */
private function buildDirTree(string $dir): array
{
$tree = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
$relPath = str_replace('\\', '/', substr($file->getPathname(), strlen($this->srcDir) + 1));
if ($this->isExcluded($relPath)) continue;
$parts = explode('/', $relPath);
$node = &$tree;
foreach ($parts as $i => $part) {
$isFile = $file->isFile() && $i === count($parts) - 1;
$key = $isFile ? 'files' : 'dirs';
if (!isset($node[$key][$part])) {
$node[$key][$part] = $isFile ? $relPath : ['dirs' => [], 'files' => []];
}
$node = &$node[$key][$part];
}
}
return $tree;
}
private function renderToc(array $tree, int $depth = 0): string
{
$html = '';
$indent = str_repeat(' ', $depth);
// 目錄
foreach ($tree['dirs'] ?? [] as $name => $subTree) {
$html .= "{$indent}\n";
$html .= "{$indent} \n";
$html .= "{$indent} \n";
$html .= $this->renderToc($subTree, $depth + 2);
$html .= "{$indent}
\n";
$html .= "{$indent} \n";
}
// 文件
foreach ($tree['files'] ?? [] as $name => $path) {
$title = basename($name, '.html');
$title = basename($title, '.htm');
$html .= "{$indent}\n";
}
return $html;
}
private function isExcluded(string $relPath): bool
{
foreach ($this->excludeDirs as $d) {
if (strpos($relPath, $d . '/') === 0) return true;
}
foreach ($this->excludeFiles as $f) {
if (basename($relPath) === $f) return true;
}
return false;
}
}
?>
<?php
require 'ChmBuilder.php';
$builder = new ChmBuilder(
srcDir: __DIR__ . '/docs', // 包含 HTML 文檔的目錄
outDir: __DIR__ . '/output', // 輸出目錄
chmName: 'my_manual', // 生成 my_manual.chm
title: '我的項目文檔',
firstPage: 'index.html'
);
if ($builder->build()) {
echo "CHM 生成成功: {$builder->outDir}/{$builder->chmName}.chm\n";
} else {
echo "生成失敗,請檢查 hhc.exe 是否安裝\n";
}
?>
| 要求 | 說明 |
|---|---|
| Windows 環境 | CHM 編譯器 hhc.exe 僅運行於 Windows |
| HTML Help Workshop | 微軟官方工具,必須安裝 下載地址 |
| 文檔編碼 | 源 HTML 建議 UTF-8,hhc.exe 會自動處理 |
| 路徑長度 | Windows 260 字符限制,目錄不要太深 |
CHM 格式雖老但仍在企業內部文檔、離線幫助系統中廣泛使用,PHP 自動化構建能大幅提升文檔發布效率。