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

yunnysunny / bookforge / 22036642206

15 Feb 2026 01:38PM UTC coverage: 70.995% (+14.8%) from 56.206%
22036642206

Pull #2

github

yunnysunny
feat: fix html generate test
Pull Request #2: feat: refactor ui flow

63 of 81 branches covered (77.78%)

Branch coverage included in aggregate %.

98 of 169 new or added lines in 13 files covered. (57.99%)

4 existing lines in 3 files now uncovered.

358 of 512 relevant lines covered (69.92%)

27.05 hits per line

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

83.02
/src/utils/markdown.ts
1
import { readdir, stat, readFile } from 'fs/promises';
2
import { join, dirname } from 'path';
3
import { getNotionDBFile, isExist, isMarkdownFile, isSpecialCVSFile } from '.';
4
import { type Child, RelationManager } from './relation';
5
import { parseFile } from '@fast-csv/parse';
6
export type NotionDBRow = object;
7
export interface NotionDBRecord extends NotionDBRow {
8
  relativePath: string;
9
}
10
export interface NotionDB {
11
  filePath: string;
12
  name: string;
13
  rows: NotionDBRecord[];
14
}
15
export interface MarkdownUtilsOptions {
16
  ignorePatterns?: string[];
17
  inputPath: string;
18
}
19
export class MarkdownRelationManager {
20
  private readonly relationManager: RelationManager = new RelationManager();
3✔
21
  private readonly notionDBs: Map<string, NotionDB> = new Map();
3✔
22
  public readonly inputPath: string;
23
  public readonly ignorePatterns?: string[];
24
  public constructor(options: MarkdownUtilsOptions) {
25
    this.inputPath = options.inputPath;
3✔
26
    this.ignorePatterns = options.ignorePatterns;
3✔
27
  }
28
  public async parseRelations(): Promise<string[]> {
29
    await this.visitAllMarkdownFiles(this.inputPath);
3✔
30
    return this.relationManager.getTopEntities();
3✔
31
  }
32
  public getChildren(parentId: string): Child[] {
33
    return this.relationManager.getChildren(parentId);
12✔
34
  }
35
  public getNotionDB(notionDBFilePath: string): NotionDB | undefined {
36
    return this.notionDBs.get(notionDBFilePath);
3✔
37
  }
38
  /**
39
   * 获取所有 markdown 文件
40
   */
41
  private async visitAllMarkdownFiles(dirPath: string): Promise<void> {
42
    const ignorePatterns = this.ignorePatterns;
9✔
43
    try {
9✔
44
      const items = await readdir(dirPath);
9✔
45

46
      for (const item of items) {
9✔
47
        const itemPath = join(dirPath, item);
24✔
48
        const fileStat = await stat(itemPath);
24✔
49

50
        if (fileStat.isDirectory()) {
24✔
51
          // 跳过忽略的目录
52
          if (ignorePatterns?.some((pattern) => item.includes(pattern))) {
6✔
53
            continue;
×
54
          }
55
          await this.visitAllMarkdownFiles(itemPath);
6✔
56
        } else if (fileStat.isFile()) {
18✔
57
          if (isMarkdownFile(itemPath)) {
18✔
58
            await this.parseInnerLinks(itemPath);
12✔
59
          } else if (isSpecialCVSFile(itemPath)) {
6✔
60
            const name = await getNotionDBFile(itemPath);
3✔
61
            if (!name) {
3✔
NEW
62
              continue;
×
63
            }
64
            await this.parseNotionDBFile(itemPath, name);
3✔
65
          }
66
        }
67
      }
68
    } catch (error) {
69
      console.warn(`无法读取目录: ${dirPath}`, error);
×
70
    }
71
  }
72

73
  private async parseNotionDBFile(notionDBFilePath: string, name: string): Promise<void> {
74
    const rows: NotionDBRow[] = [];
3✔
75
    await new Promise((resolve, reject) => {
3✔
76
      parseFile(notionDBFilePath, { headers: true })
3✔
77
        .on('data', (row: NotionDBRow) => {
78
          // const childLink = join(name, row.Name);
79
          // const childPath = join(dirname(notionDBFilePath), childLink);
80
          rows.push(row);
9✔
81
          // this.relationManager.addRelation({
82
          //   parentId: notionDBFilePath,
83
          //   childId: childPath,
84
          //   relativePath: childLink,
85
          // });
86
        })
87
        .on('end', () => {
88
          resolve(undefined);
3✔
89
        })
90
        .on('error', (error) => {
NEW
91
          reject(error);
×
92
        });
93
    });
94
    if (rows.length === 0) {
3✔
NEW
95
      return;
×
96
    }
97
    const firstRow = rows[0] as Record<string, string>;
3✔
98
    const keys = Object.keys(firstRow);
3✔
99
    const firstKey = keys[0];
3✔
100

101
    const names = rows.map((row: any) => {
3✔
102
      const rowData = row as Record<string, string>;
9✔
103
      return rowData[firstKey];
9✔
104
    });
105
    const baseDir = dirname(notionDBFilePath);
3✔
106
    let dbDir = join(baseDir, name);
3✔
107
    const handle = await stat(dbDir);
3✔
108
    const hasDbDir = handle.isDirectory();
3✔
109
    if (!hasDbDir) {
3✔
NEW
110
      dbDir = baseDir;
×
111
    }
112
    const files = await readdir(dbDir);
3✔
113
    if (files.length === 0) {
3✔
NEW
114
      return;
×
115
    }
116
    const records: NotionDBRecord[] = [];
3✔
117
    const row2PathMap = new Map<
3✔
118
      string,
119
      {
120
        childId: string;
121
        relativePath: string;
122
        row: object;
123
      }
124
    >();
125
    files.forEach((file) => {
3✔
126
      const path = join(dbDir, file);
9✔
127
      if (!isMarkdownFile(path)) {
9✔
NEW
128
        return;
×
129
      }
130
      const dbName = file.split(' ')[0];
9✔
131
      const index = names.indexOf(dbName);
9✔
132
      if (index === -1) {
9✔
NEW
133
        return;
×
134
      }
135
      const row = rows[index];
9✔
136
      const relativePath = hasDbDir ? join(name, file) : file;
9✔
137
      const childId = path;
9✔
138

139
      row2PathMap.set(dbName, {
9✔
140
        childId,
141
        relativePath,
142
        row,
143
      });
144
    });
145
    names.forEach((name) => {
3✔
146
      const data = row2PathMap.get(name);
9✔
147
      if (!data) {
9✔
NEW
148
        return;
×
149
      }
150
      records.push({
9✔
151
        ...data.row,
152
        relativePath: data.relativePath,
153
      });
154
      this.relationManager.addRelation({
9✔
155
        parentId: notionDBFilePath,
156
        childId: data.childId,
157
        relativePath: data.relativePath,
158
      });
159
    });
160
    this.notionDBs.set(notionDBFilePath, {
3✔
161
      rows: records,
162
      name,
163
      filePath: notionDBFilePath,
164
    });
165
  }
166

167
  /**
168
   * 解析内部链接
169
   */
170
  private async parseInnerLinks(markdownFilePath: string): Promise<void> {
171
    const content = await readFile(markdownFilePath, 'utf-8');
12✔
172
    const lines = content.split('\n');
12✔
173

174
    for (const line of lines) {
12✔
175
      const trimmedLine = line.trim();
288✔
176
      const linkMatch = trimmedLine.match(/\[([^\]]+)\]\(([^)]+)\)/);
288✔
177
      if (!linkMatch) {
288✔
178
        continue;
273✔
179
      }
180
      const _title = linkMatch[1];
15✔
181
      const link = linkMatch[2];
15✔
182

183
      // 解析链接的 markdown 文件
184
      const childPath = join(dirname(markdownFilePath), decodeURIComponent(link));
15✔
185
      if (!isMarkdownFile(childPath)) {
15✔
186
        continue;
15✔
187
      }
188
      try {
×
189
        const fileStat = await stat(childPath);
×
190
        if (!fileStat.isFile()) {
×
191
          continue;
×
192
        }
193
      } catch (error) {
194
        console.warn(`无法读取文件: ${childPath}`, error);
×
195
        continue;
×
196
      }
197
      this.relationManager.addRelation({
×
198
        parentId: markdownFilePath,
199
        childId: childPath,
200
        relativePath: link,
201
      });
202
    }
203
  }
204
}
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