// darkBidChecker.js // 逻辑已迁移至项目内 Python:modules/dark_bid_format_check.py(本文件保留作参考) // 依赖: jsdom (npm install jsdom) const { JSDOM } = require('jsdom'); /** * 暗标格式检查器 * @param {string} htmlContent - 技术暗标的HTML内容(由Word/PDF导出的完整HTML) * @param {Object} options - 可选配置 * @returns {Object} 符合格式的JSON检查报告 */ function checkTechnicalBid(htmlContent, options = {}) { const dom = new JSDOM(htmlContent); const document = dom.window.document; const styleSheets = Array.from(document.styleSheets); // 辅助函数:获取元素实际渲染样式(jsdom支持有限,但可获取内联和style标签定义) function getStyle(element, property) { return dom.window.getComputedStyle(element).getPropertyValue(property); } // 结果收集器 const results = { overall: true, details: [], violations: [] }; // 通用添加结果方法 function addResult(ruleName, passed, message, elements = []) { results.details.push({ rule: ruleName, passed, message }); if (!passed) { results.overall = false; results.violations.push({ rule: ruleName, message, elements: elements.map(el => el.outerHTML.slice(0, 200)) }); } } // ========== 1. 检查是否存在投标人身份信息 ========== function checkIdentityInfo() { const bodyText = document.body.innerText; // 公司名称模式(可扩展) const companyPattern = /(?:我公司|本公司|[((]?[A-Za-z\u4e00-\u9fa5]+(?:集团|股份|有限|责任|公司)[))]?)/g; // 地址模式(省市区路号等) const addrPattern = /(?:省|市|区|县|镇|路|街|大道|号|大厦|楼|层)[\u4e00-\u9fa50-9]+/g; // 总监/专监真实姓名模式(除甲、乙等代称外) const namePattern = /(?:总监理工程师|专业监理工程师|技术负责人|项目经理)[::]\s*[^甲乙丙丁戊己庚辛壬癸\s]{2,4}(?=[,。;\s]|$)/g; let foundCompany = false; let foundAddr = false; let foundRealName = false; if (companyPattern.test(bodyText)) foundCompany = true; if (addrPattern.test(bodyText)) foundAddr = true; if (namePattern.test(bodyText)) foundRealName = true; // 检查图片alt或title是否包含公司标识 const images = document.querySelectorAll('img'); let hasLogo = false; images.forEach(img => { const alt = img.alt || ''; const src = img.src || ''; if (/logo|商标|微标|公司|品牌/i.test(alt) || /logo/i.test(src)) hasLogo = true; }); const passed = !(foundCompany || foundAddr || foundRealName || hasLogo); addResult('身份信息隐藏', passed, passed ? '未发现投标人身份信息' : '发现投标人身份信息(公司名/地址/真实姓名/商标)'); } // ========== 2. 标题格式检查(三号黑体,非斜体,无下划线等) ========== function checkHeadings() { // 标题选择器:h1-h6 或任何 role="heading" 或具有大纲级别样式的元素 const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6, [role="heading"], .heading, .title'); let allValid = true; const invalidHeadings = []; headings.forEach(heading => { const fontSize = getStyle(heading, 'font-size'); const fontFamily = getStyle(heading, 'font-family').toLowerCase(); const fontStyle = getStyle(heading, 'font-style'); const textDecoration = getStyle(heading, 'text-decoration'); const color = getStyle(heading, 'color'); const fontWeight = getStyle(heading, 'font-weight'); // 三号 ≈ 16pt (21.33px) 允许误差 ±2px const sizeOk = Math.abs(parseFloat(fontSize) - 21.33) <= 3; const fontOk = fontFamily.includes('黑体') || fontFamily.includes('simhei') || fontFamily.includes('microsoft yahei'); const styleOk = fontStyle !== 'italic'; const decorOk = !textDecoration.includes('underline'); const colorOk = color === 'rgb(0, 0, 0)' || color === '#000000'; const weightOk = fontWeight !== '400' && fontWeight !== 'normal'; // 黑体通常加粗 if (!(sizeOk && fontOk && styleOk && decorOk && colorOk && weightOk)) { allValid = false; invalidHeadings.push(heading); } }); addResult('标题格式', allValid, allValid ? '所有标题符合三号黑体要求' : '部分标题字号/字体/颜色/下划线不符合要求', invalidHeadings); } // ========== 3. 正文格式检查(四号宋体,首行缩进2字符,行距固定值26磅,无着色等) ========== function checkBodyText() { const bodyElements = document.querySelectorAll('p, div, span, li, td, th'); let allValid = true; const invalidElements = []; // 排除标题、页眉页脚、目录等 const excludeSelectors = 'h1, h2, h3, h4, h5, h6, .header, .footer, .toc, .目录'; bodyElements.forEach(el => { if (el.matches(excludeSelectors)) return; const text = el.innerText.trim(); if (text.length === 0) return; const fontSize = getStyle(el, 'font-size'); const fontFamily = getStyle(el, 'font-family').toLowerCase(); const color = getStyle(el, 'color'); const textIndent = getStyle(el, 'text-indent'); const lineHeight = getStyle(el, 'line-height'); const textDecoration = getStyle(el, 'text-decoration'); const fontWeight = getStyle(el, 'font-weight'); const fontStyle = getStyle(el, 'font-style'); // 四号 ≈ 14pt (18.67px) const sizeOk = Math.abs(parseFloat(fontSize) - 18.67) <= 2; const fontOk = fontFamily.includes('宋体') || fontFamily.includes('simsun') || fontFamily.includes('serif'); const colorOk = color === 'rgb(0, 0, 0)' || color === '#000000'; // 首行缩进2字符(以em为单位,2em代表2个汉字) const indentOk = parseFloat(textIndent) >= 1.8 && parseFloat(textIndent) <= 2.2; // 行距固定值26磅 (34.67px) const lineHeightOk = Math.abs(parseFloat(lineHeight) - 34.67) <= 2; const decorOk = !textDecoration.includes('underline'); const weightOk = fontWeight === '400' || fontWeight === 'normal'; const styleOk = fontStyle !== 'italic'; if (!(sizeOk && fontOk && colorOk && indentOk && lineHeightOk && decorOk && weightOk && styleOk)) { allValid = false; invalidElements.push(el); } }); addResult('正文格式', allValid, allValid ? '所有正文符合四号宋体/缩进/行距/颜色要求' : '部分正文段落格式不符合要求', invalidElements); } // ========== 4. 目录检查(无页码,无页眉页脚) ========== function checkTOC() { const tocElements = document.querySelectorAll('.toc, .table-of-contents, .目录, [role="directory"]'); let noPageNumbers = true; let noHeaderFooter = true; tocElements.forEach(toc => { const text = toc.innerText; // 检查是否存在页码(数字独立在行尾或制表符后) if (/\d+$/.test(text.trim()) || /\.{2,}\s*\d+/.test(text)) { noPageNumbers = false; } // 检查内部是否有页眉页脚元素 if (toc.querySelector('.header, .footer, .page-header, .page-footer')) { noHeaderFooter = false; } }); // 若没有目录元素,按规则应存在目录但不得有页码,这里假设必须存在目录(招标要求通常有目录) if (tocElements.length === 0) { addResult('目录要求', false, '未检测到目录,请确保包含目录且目录无页码无页眉页脚'); } else { const passed = noPageNumbers && noHeaderFooter; addResult('目录要求', passed, passed ? '目录符合无页码、无页眉页脚要求' : '目录中存在页码或页眉页脚'); } } // ========== 5. 图表位置及图表内文字格式 ========== function checkChartsAndTables() { // 定位附件/附表章节 const appendix = document.querySelector('#appendix, .appendix, .attachment, 附件, 附表'); const isInAppendix = (el) => appendix && appendix.contains(el); const allTables = document.querySelectorAll('table'); const allImages = document.querySelectorAll('img'); const allFigures = document.querySelectorAll('figure, .chart'); let illegalCharts = []; // 正文中不允许有图表,除非在附件内 [...allTables, ...allImages, ...allFigures].forEach(chart => { if (!isInAppendix(chart)) { illegalCharts.push(chart); } }); let chartTextValid = true; // 附件内图表文字需五号宋体 if (appendix) { const chartTexts = appendix.querySelectorAll('table, td, th, figcaption, .chart-text'); chartTexts.forEach(el => { const fontSize = getStyle(el, 'font-size'); const fontFamily = getStyle(el, 'font-family').toLowerCase(); const color = getStyle(el, 'color'); const sizeOk = Math.abs(parseFloat(fontSize) - 10.5) <= 1.5; // 五号=10.5pt const fontOk = fontFamily.includes('宋体') || fontFamily.includes('simsun'); const colorOk = color === 'rgb(0, 0, 0)' || color === '#000000'; if (!(sizeOk && fontOk && colorOk)) chartTextValid = false; }); } const chartsPassed = illegalCharts.length === 0 && chartTextValid; addResult('图表规范', chartsPassed, chartsPassed ? '图表仅出现在附件/附表内,且图表文字符合五号宋体' : `正文中发现${illegalCharts.length}个图表或附件内图表文字格式错误`, illegalCharts); } // ========== 6. 全文字体颜色检查(无彩色,无着重号,无下划线) ========== function checkColorsAndDecorations() { const allElements = document.querySelectorAll('*'); let colorViolations = []; let decorationViolations = []; allElements.forEach(el => { const color = getStyle(el, 'color'); if (color !== 'rgb(0, 0, 0)' && color !== '#000000' && color !== 'black') { if (el.innerText.trim().length > 0) colorViolations.push(el); } const textDecor = getStyle(el, 'text-decoration'); if (textDecor.includes('underline')) decorationViolations.push(el); // 着重号检测(一般使用伪元素或border-bottom,简单检测样式) const borderBottom = getStyle(el, 'border-bottom-style'); if (borderBottom === 'solid' || borderBottom === 'dotted') { decorationViolations.push(el); } }); const passed = colorViolations.length === 0 && decorationViolations.length === 0; addResult('颜色与装饰', passed, passed ? '无彩色文字、无下划线、无着重号' : `发现${colorViolations.length}处彩色文字,${decorationViolations.length}处下划线/着重号`, [...colorViolations, ...decorationViolations]); } // ========== 7. 页面设置检查(A4纵向,页边距) ========== function checkPageSetup() { // 检查@page规则或根元素margin let pageValid = true; let marginTop, marginBottom, marginLeft, marginRight; // 尝试从styleSheets中获取@page let pageRule = null; for (let sheet of styleSheets) { try { const rules = sheet.cssRules || sheet.rules; for (let rule of rules) { if (rule.type === CSSRule.PAGE_RULE) { pageRule = rule.style; break; } } } catch(e) { /* 跨域限制忽略 */ } } if (pageRule) { marginTop = pageRule.marginTop; marginBottom = pageRule.marginBottom; marginLeft = pageRule.marginLeft; marginRight = pageRule.marginRight; const size = pageRule.size; if (size && size.toLowerCase() !== 'a4') pageValid = false; } else { // 检查body或根容器的margin const bodyStyle = getStyle(document.body, 'margin'); if (bodyStyle) { const margins = bodyStyle.split(' '); // 简单近似 marginTop = margins[0]; marginBottom = margins[2] || margins[0]; marginLeft = margins[3] || margins[1]; marginRight = margins[1]; } else { pageValid = false; } } const topOk = marginTop === '2.54cm' || parseFloat(marginTop) === 2.54; const bottomOk = marginBottom === '2.54cm' || parseFloat(marginBottom) === 2.54; const leftOk = marginLeft === '3.18cm' || parseFloat(marginLeft) === 3.18; const rightOk = marginRight === '3.18cm' || parseFloat(marginRight) === 3.18; const pageOrientation = document.documentElement.style.width === 'auto' ? '纵向' : '横向'; // 近似 const passed = topOk && bottomOk && leftOk && rightOk && pageOrientation !== '横向'; addResult('页面设置', passed, passed ? '页面设置符合A4纵向/边距要求' : '页面边距或纸张方向不符合要求'); } // 执行所有检查 checkIdentityInfo(); checkHeadings(); checkBodyText(); checkTOC(); checkChartsAndTables(); checkColorsAndDecorations(); checkPageSetup(); return results; } // 导出函数供外部使用 module.exports = { checkTechnicalBid }; // ==================== 使用示例 ==================== // 假设已有htmlContent(技术暗标HTML字符串) // const report = checkTechnicalBid(htmlContent); // console.log(JSON.stringify(report, null, 2));