• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yunnysunny / bookforge / 20639889242

01 Jan 2026 02:03PM UTC coverage: 55.087% (-36.6%) from 91.667%
20639889242

push

github

web-flow
feat: add notion support

27 of 51 branches covered (52.94%)

Branch coverage included in aggregate %.

75 of 221 new or added lines in 12 files covered. (33.94%)

195 of 352 relevant lines covered (55.4%)

23.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.22
/src/generators/html.generator.ts
1
// HTML 生成器
2

3
import { copyFile, writeFile } from 'fs/promises';
4
import { join } from 'path';
5
// import { fileURLToPath } from 'url';
6
import type { BookForgeConfig, Heading, TreeNode } from '../types/index.js';
7
import { AbstractGenerator } from './abstract.generator.js';
8

9
export class HtmlGenerator extends AbstractGenerator {
10
  private sidebar: string = '';
15✔
11
  protected async doGenerate(treeRoot: TreeNode): Promise<void> {
12
    this.sidebar = await this.generateSidebar(treeRoot);
3✔
13
    // 生成主页面
14
    await this.generateIndexPage(treeRoot);
3✔
15

16
    // 生成各个文档页面
17
    await this.generateDocumentPages(treeRoot);
3✔
18

19
    // 生成样式文件
20
    await this.copyStyles();
3✔
21

22
    // 生成脚本文件
23
    await this.copyScripts();
3✔
24
  }
25
  constructor(config: BookForgeConfig) {
26
    super(config);
15✔
27
    this.name = 'html';
15✔
28
  }
29

30
  /**
31
   * 生成主页面
32
   */
33
  private async generateIndexPage(tree: TreeNode): Promise<void> {
34
    const html = await this.generateSinglePageHtml({
3✔
35
      title: this.title,
36
      path: tree.children[0]?.path as string,
37
      content: tree.children[0]?.content || '',
38
      headings: tree.children[0]?.headings || [],
39
      children: tree.children[0]?.children || [],
40
    });
41
    const indexPath = join(this.outputDir, 'index.html');
3✔
42
    await writeFile(indexPath, html, 'utf-8');
3✔
43
  }
44

45
  /**
46
   * 生成文档页面
47
   */
48
  private async generateDocumentPages(treeRoot: TreeNode): Promise<void> {
49
    await Promise.all(
3✔
50
      treeRoot.children.map(async (node) => {
51
        if (node.content) {
6✔
52
          const html = await this.generateSinglePageHtml(node);
6✔
53
          const fileName = `${this.getFileName(node)}.html`;
6✔
54
          const filePath = join(this.outputDir, fileName);
6✔
55
          await writeFile(filePath, html, 'utf-8');
6✔
56
          this.logger.info(`Generated document page: ${fileName}`);
6✔
57
        }
58
        if (node.children.length > 0) {
6✔
NEW
59
          await this.generateDocumentPages(node);
×
60
        }
61
      }),
62
    );
63
  }
64

65
  /**
66
   * 生成 HTML 模板
67
   */
68
  private async generateSinglePageHtml(node: TreeNode): Promise<string> {
69
    const toc = node.headings
12✔
70
      ? await this.generateTableOfContents(node.headings)
71
      : '';
72
    const htmlContent = await this.bookParser.toHtml(node);
12✔
73
    const html = await this.render('page.ejs', {
12✔
74
      title: node.title,
75
      sidebar: this.sidebar,
76
      toc,
77
      htmlContent,
78
    });
79

80
    return html;
12✔
81
  }
82

83
  /**
84
   * 生成侧边栏
85
   */
86
  private async generateSidebar(tree: TreeNode): Promise<string> {
87
    return await this.generateSidebarItems(tree.children, 0);
6✔
88
  }
89

90
  /**
91
   * 生成侧边栏项目
92
   */
93
  private async generateSidebarItems(
94
    nodes: TreeNode[],
95
    level: number,
96
  ): Promise<string> {
97
    //     let html = '';
98

99
    //     for (const node of nodes) {
100
    //       const indent = '  '.repeat(level);
101
    //       const fileName = node.path ? this.getFileName(node.title) + '.html' : 'index.html';
102

103
    //       html += `${indent}<div class="sidebar-item level-${level}">
104
    // ${indent}  <a href="${fileName}" class="sidebar-link">${node.title}</a>
105
    // `;
106

107
    //       if (node.children.length > 0) {
108
    //         html += `${indent}  <div class="sidebar-children">
109
    // ${this.generateSidebarItems(node.children, level + 1)}
110
    // ${indent}  </div>
111
    // `;
112
    //       }
113

114
    //       html += `${indent}</div>
115
    // `;
116
    //     }
117
    const html = await this.render('left-side.ejs', {
6✔
118
      nodes,
119
      level,
120
      getFileName: this.getFileName.bind(this),
121
    });
122
    // console.log(nodes.map(node => node.title), '-->', html);
123
    return html;
6✔
124
  }
125

126
  /**
127
   * 生成目录
128
   */
129
  private async generateTableOfContents(headings: Heading[]): Promise<string> {
130
    return await this.generateTocItems(headings, 0);
18✔
131
  }
132

133
  /**
134
   * 生成目录项目
135
   */
136
  private async generateTocItems(
137
    headings: Heading[],
138
    level: number,
139
  ): Promise<string> {
140
    //     let html = '<ul class="toc-list">';
141

142
    //     for (const heading of headings) {
143
    //       html += `<li class="toc-item level-${heading.level}">
144
    //         <a href="#${heading.id}" class="toc-link">${heading.text}</a>
145
    // `;
146

147
    //       if (heading.children.length > 0) {
148
    //         html += this.generateTocItems(heading.children, level + 1);
149
    //       }
150

151
    //       html += '</li>';
152
    //     }
153

154
    //     html += '</ul>';
155
    const html = await this.render('toc.ejs', {
18✔
156
      headings,
157
      level,
158
    });
159
    return html;
18✔
160
  }
161
  private async copyFile(src: string, dest: string): Promise<void> {
162
    await copyFile(join(__dirname, 'static', src), join(this.outputDir, dest));
6✔
163
  }
164

165
  /**
166
   * 生成样式文件
167
   */
168
  private async copyStyles(): Promise<void> {
169
    await this.copyFile('styles.css', 'styles.css');
3✔
170
  }
171

172
  /**
173
   * 生成脚本文件
174
   */
175
  private async copyScripts(): Promise<void> {
176
    await this.copyFile('script.js', 'script.js');
3✔
177
  }
178
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc