const fs = require('fs-extra'); const path = require('path'); const moment = require('moment'); const chalk = require('chalk'); /** * HTML格式的源代码文档生成器 * 避免PDF中文乱码问题,使用HTML+CSS确保中文正确显示 * @param {Object} projectInfo 项目信息 * @param {string} outputDir 输出目录 * @returns {Promise} 生成的HTML文件路径 */ async function exportSourceCodeHTML(projectInfo, outputDir) { try { console.log(chalk.yellow('💻 生成HTML源代码文档...')); const timestamp = moment().format('YYYYMMDD_HHMMSS'); const fileName = `源代码文档_${projectInfo.name}_${timestamp}.html`; const outputPath = path.join(outputDir, fileName); // 生成HTML内容 const htmlContent = await generateSourceCodeHTML(projectInfo); // 写入文件 await fs.writeFile(outputPath, htmlContent, 'utf8'); console.log(chalk.green(`✅ HTML源代码文档已生成: ${outputPath}`)); console.log(chalk.blue('💡 提示: 在浏览器中打开HTML文件,然后打印为PDF即可获得符合要求的软著材料')); return outputPath; } catch (error) { console.error(chalk.red('❌ HTML源代码文档生成失败:'), error.message); throw error; } } /** * 生成源代码文档的HTML内容 * @param {Object} projectInfo 项目信息 * @returns {Promise} HTML内容 */ async function generateSourceCodeHTML(projectInfo) { const timestamp = moment().format('YYYY年MM月DD日 HH:mm:ss'); // 设置CSS变量用于页眉页脚 const softwareName = getSoftwareName(projectInfo.name); const version = await getProjectVersion(projectInfo); const headerText = `${softwareName}_${version}`; const footerText = `软件著作权申请材料 | 生成时间: ${timestamp}`; return ` ${softwareName}源代码文档
${softwareName}源代码文档
版本 ${version}
生成日期: ${timestamp}
项目名称
${projectInfo.name}
源代码文件
${projectInfo.files.length} 个
代码行数
${projectInfo.totalLines} 行
主要语言
${projectInfo.languages.join(', ')}
源代码文件列表
    ${projectInfo.files.map((file, index) => `
  • ${index + 1}. ${file.relativePath || file.path}
    语言: ${file.language || '未知'} | 代码行数: ${file.lines} 行 | 文件大小: ${formatFileSize(file.size)}
  • `).join('')}
${generateFullSourceCode(projectInfo, 50)}
`; } /** * 生成示例代码 * @param {Object} file 文件信息 * @returns {string} 示例代码HTML */ /** * 简单的语法高亮 * @param {string} code 代码行 * @returns {string} 高亮后的HTML */ function highlightSyntax(code) { return code .replace(/\/\/(.*)$/g, '//$1') .replace(/function\s+(\w+)/g, 'function $1') .replace(/return/g, 'return') .replace(/const/g, 'const') .replace(/console/g, 'console') .replace(/"([^"]*)"/g, '"$1"') .replace(/'([^']*)'/g, '\'$1\''); } /** * 格式化文件大小 * @param {number} bytes 字节数 * @returns {string} 格式化的大小 */ function formatFileSize(bytes) { if (!bytes) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } /** * 获取软件名称 * @param {string} projectName 项目名称 * @returns {string} 软件名称 */ function getSoftwareName(projectName) { // 清理项目名称,移除常见后缀 const cleanedName = projectName .replace(/[-_]app$/i, '') .replace(/[-_]project$/i, '') .replace(/[-_]system$/i, '') .replace(/ai-agent/i, 'AI智能体') .replace(/-/g, ' '); // 首字母大写 return cleanedName.charAt(0).toUpperCase() + cleanedName.slice(1); } /** * 从项目中读取版本信息 * @param {Object} projectInfo 项目信息 * @returns {Promise} 版本号 */ async function getProjectVersion(projectInfo) { try { const projectPath = projectInfo.path || process.cwd(); // 尝试从package.json读取版本 const packageJsonPath = path.join(projectPath, 'package.json'); if (await fs.pathExists(packageJsonPath)) { const packageData = await fs.readJson(packageJsonPath); if (packageData.version) { return `V${packageData.version}`; } } // 尝试从setup.py读取版本 const setupPyPath = path.join(projectPath, 'setup.py'); if (await fs.pathExists(setupPyPath)) { const setupContent = await fs.readFile(setupPyPath, 'utf8'); const versionMatch = setupContent.match(/version\s*=\s*['"]([^'"]+)['"]/); if (versionMatch) { return `V${versionMatch[1]}`; } } // 尝试从Cargo.toml读取版本 const cargoTomlPath = path.join(projectPath, 'Cargo.toml'); if (await fs.pathExists(cargoTomlPath)) { const cargoContent = await fs.readFile(cargoTomlPath, 'utf8'); const versionMatch = cargoContent.match(/^version\s*=\s*["']([^"']+)["']/m); if (versionMatch) { return `V${versionMatch[1]}`; } } // 尝试从requirements.txt或pyproject.toml读取 const pyprojectPath = path.join(projectPath, 'pyproject.toml'); if (await fs.pathExists(pyprojectPath)) { const pyprojectContent = await fs.readFile(pyprojectPath, 'utf8'); const versionMatch = pyprojectContent.match(/version\s*=\s*["']([^"']+)["']/); if (versionMatch) { return `V${versionMatch[1]}`; } } // 尝试从Go.mod读取版本 const goModPath = path.join(projectPath, 'go.mod'); if (await fs.pathExists(goModPath)) { const goModContent = await fs.readFile(goModPath, 'utf8'); // Go module版本通常在go.mod文件中不明确指定,返回默认值 console.log('Go项目检测到,使用默认版本'); } // 尝试从README文件读取版本 const readmeFiles = ['README.md', 'README.txt', 'readme.md', 'README']; for (const readmeFile of readmeFiles) { const readmePath = path.join(projectPath, readmeFile); if (await fs.pathExists(readmePath)) { const readmeContent = await fs.readFile(readmePath, 'utf8'); // 尝试多种版本号格式 const versionPatterns = [ /(?:version|版本)[\s::]*v?(\d+\.\d+\.\d+)/i, /^#+\s*v?(\d+\.\d+\.\d+)/m, /\*\*(?:version|版本)\*\*[\s::]*v?(\d+\.\d+\.\d+)/i, /\[v?(\d+\.\d+\.\d+)\]/, ]; for (const pattern of versionPatterns) { const versionMatch = readmeContent.match(pattern); if (versionMatch) { console.log(`从${readmeFile}读取到版本号: ${versionMatch[1]}`); return `V${versionMatch[1]}`; } } } } // 默认版本 return 'V1.0.0'; } catch (error) { console.warn('读取版本信息失败,使用默认版本:', error.message); return 'V1.0.0'; } } // 从scanner.js导入支持的扩展 const SUPPORTED_EXTENSIONS = { '.js': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'javascript', priority: 1 }, '.jsx': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'jsx', priority: 1 }, '.ts': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'typescript', priority: 2 }, '.tsx': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'tsx', priority: 2 }, '.css': { single_line: null, multi_line: [/\/\*/, /\*\//], language: 'css', priority: 3 }, '.scss': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'scss', priority: 3 }, '.html': { single_line: null, multi_line: [//], language: 'html', priority: 3 }, '.py': { single_line: '#', multi_line: [/"""/, /"""/], language: 'python', priority: 1 }, '.java': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'java', priority: 1 }, '.c': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'c', priority: 1 }, '.cpp': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'cpp', priority: 1 }, '.h': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'c', priority: 1 }, '.cs': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'csharp', priority: 1 }, '.php': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'php', priority: 1 }, '.rb': { single_line: '#', multi_line: [/=begin/, /=end/], language: 'ruby', priority: 1 }, '.go': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'go', priority: 1 }, '.rs': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'rust', priority: 1 }, '.kt': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'kotlin', priority: 1 }, '.swift': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'swift', priority: 1 }, '.vue': { single_line: '//', multi_line: [/\/\*/, /\*\//], language: 'vue', priority: 1 } }; /** * 计算软著页数要求 * @param {number} totalLines 总代码行数 * @param {number} linesPerPage 每页行数(默认50) * @returns {Object} 页数配置 */ function calculatePages(totalLines, linesPerPage = 50) { const totalPages = Math.ceil(totalLines / linesPerPage); if (totalPages <= 60) { return { showAll: true, startPage: 1, endPage: totalPages, totalPages: totalPages }; } else { return { showAll: false, startPage: 1, endPage: 30, totalPages: totalPages, showLast: true, lastStartPage: totalPages - 29, lastEndPage: totalPages }; } } /** * 清理代码内容,移除注释和空白行 * @param {string} content 原始代码内容 * @param {string} extension 文件扩展名 * @returns {string} 清理后的代码内容 */ function cleanCodeContent(content, extension) { if (!content) return content; const langInfo = SUPPORTED_EXTENSIONS[extension.toLowerCase()]; if (!langInfo) return content; let cleanedContent = content; // 1. 删除多行注释 /\*(.|\r\n|\n)*?\*/ if (langInfo.multi_line && langInfo.multi_line.length >= 2) { const [startPattern, endPattern] = langInfo.multi_line; const multiLineRegex = new RegExp( startPattern.source + '[\\s\\S]*?' + endPattern.source, 'g' ); cleanedContent = cleanedContent.replace(multiLineRegex, ''); } // 2. 删除单行注释 //.* if (langInfo.single_line) { const singleLineRegex = new RegExp(langInfo.single_line + '.*$', 'gm'); cleanedContent = cleanedContent.replace(singleLineRegex, ''); } // 3. 删除空白行 ^\\s*(?=\\r?$)\\n cleanedContent = cleanedContent.replace(/^\s*$(?:\r\n?|\n)/gm, ''); // 4. 删除版权声明和作者信息 const copyrightPatterns = [ /copyright[\s\S]*?$/gmi, /author[\s\S]*?$/gmi, /license[\s\S]*?$/gmi, /\*\s*@\w+[\s\S]*?\*/g, /spdx-license-identifier[\s\S]*?$/gmi ]; copyrightPatterns.forEach(pattern => { cleanedContent = cleanedContent.replace(pattern, ''); }); return cleanedContent.trim(); } /** * 生成完整的源代码展示 * @param {Object} projectInfo 项目信息 * @param {number} linesPerPage 每页行数 * @returns {string} HTML内容 */ function generateFullSourceCode(projectInfo, linesPerPage = 50) { const pageConfig = calculatePages(projectInfo.totalLines, linesPerPage); let htmlContent = ''; // 页面统计信息 htmlContent += `
源代码统计
项目名称
${projectInfo.name}
总代码行数
${projectInfo.totalLines} 行
每页行数
${linesPerPage} 行
总页数
${pageConfig.totalPages} 页
软著页数
${pageConfig.showAll ? '全部' : `前${pageConfig.endPage}页+后${30}页`}
`; // 生成源代码内容 htmlContent += `
源代码内容
`; let currentPage = 1; let currentLineOnPage = 0; let totalLinesShown = 0; // 按优先级排序文件 const sortedFiles = projectInfo.files .filter(file => file.lines > 0) // 只处理有内容的文件 .sort((a, b) => { const langA = SUPPORTED_EXTENSIONS[path.extname(a.path).toLowerCase()]; const langB = SUPPORTED_EXTENSIONS[path.extname(b.path).toLowerCase()]; return (langA?.priority || 999) - (langB?.priority || 999); }); // 遍历文件并生成代码块 for (const file of sortedFiles) { try { // 使用正确的文件路径 - fullPath是绝对路径,path是相对路径 const filePath = file.fullPath || file.path; // 验证文件存在 if (!fs.existsSync(filePath)) { console.warn(`文件不存在: ${filePath}`); continue; } const fileContent = fs.readFileSync(filePath, 'utf8'); // 验证文件内容不为空 if (!fileContent || fileContent.trim().length === 0) { console.warn(`文件内容为空: ${filePath}`); continue; } const cleanedContent = cleanCodeContent(fileContent, path.extname(filePath)); const lines = cleanedContent.split('\n').filter(line => line.trim().length > 0); if (lines.length === 0) continue; htmlContent += `

文件: ${file.relativePath || file.path}

文件大小: ${formatFileSize(file.size)} | 有效代码: ${lines.length} 行
`; // 逐行添加代码 for (let i = 0; i < lines.length; i++) { const lineNumber = i + 1; const lineContent = lines[i]; // 检查是否需要分页 if (currentLineOnPage >= linesPerPage) { currentPage++; currentLineOnPage = 0; // 如果是前30页,继续添加 if (currentPage <= 30) { htmlContent += `

[续页 ${currentPage}] - ${file.relativePath || file.path}

`; } // 如果是超过60页的项目,需要判断是否显示最后30页 else if (pageConfig.showLast && currentPage > 30 && currentPage < (pageConfig.totalPages - 29)) { // 跳过中间部分 currentPage = pageConfig.lastStartPage; htmlContent += `
... 省略中间内容 ...
[注: 软著申请要求超过60页时,只提交前30页和后30页]

[后30页 第${currentPage - (pageConfig.lastStartPage - 1) + 1}页] - ${file.relativePath || file.path}

`; } // 如果是最后30页范围内,继续添加 else if (currentPage < pageConfig.totalPages + 30) { htmlContent += `

[后30页 第${currentPage - (pageConfig.lastStartPage - 1) + 1}页] - ${file.relativePath || file.path}

`; } // 已经显示完所有要求的页数 else { break; } } // 添加代码行 const highlightedLine = highlightSyntax(lineContent); const lineNumberStr = lineNumber.toString().padStart(4, ' '); htmlContent += ` ${lineNumberStr}:${highlightedLine} `; currentLineOnPage++; totalLinesShown++; } htmlContent += `
`; // 限制显示的文件数量(避免文档过长) if (totalLinesShown >= 15000) { // 大约300页的限制 htmlContent += `

注意:由于代码量较大,已截断显示。

完整源代码请参考项目文件,共计 ${projectInfo.files.length} 个源代码文件,${projectInfo.totalLines} 行代码。

`; break; } } catch (error) { const filePath = file.fullPath || file.path; console.error(`读取文件失败 ${filePath}:`, error.message); // 跳过这个文件,继续处理其他文件 } } htmlContent += `
`; return htmlContent; } module.exports = { exportSourceCodeHTML };