| const fs = require('fs'); | |
| const path = require('path'); | |
| const matter = require('gray-matter'); | |
| const { normalizePath } = require('./lib'); | |
| function makeFinding(entry, severity, rule, line, message) { | |
| return { | |
| severity, | |
| rule, | |
| path: entry.source_path, | |
| line, | |
| message, | |
| owner: entry.owner || '@apache/doris-website-maintainers', | |
| }; | |
| } | |
| function isLegacyVersion(version) { | |
| if (!version || version === 'current') { | |
| return false; | |
| } | |
| const match = String(version).match(/^(\d+)(?:\.(\d+))?/); | |
| if (!match) { | |
| return false; | |
| } | |
| const major = Number(match[1]); | |
| const minor = Number(match[2] || 0); | |
| return major < 2 || (major === 2 && minor <= 1); | |
| } | |
| function severityForEntry(entry) { | |
| if (isLegacyVersion(entry.version) || entry.blocking_level === 'report_only') { | |
| return 'info'; | |
| } | |
| return 'warning'; | |
| } | |
| function normalizeHeading(text) { | |
| return String(text || '') | |
| .replace(/\s*\{#[^}]+}\s*$/, '') | |
| .replace(/<[^>]+>/g, '') | |
| .replace(/[::]+$/g, '') | |
| .replace(/\s*\/\s*/g, '/') | |
| .replace(/\s+/g, ' ') | |
| .trim() | |
| .toLowerCase(); | |
| } | |
| function parseMarkdownSections(content) { | |
| const lines = String(content || '').split(/\r?\n/); | |
| const headings = []; | |
| let inFence = false; | |
| let fenceMarker = null; | |
| for (let index = 0; index < lines.length; index += 1) { | |
| const line = lines[index]; | |
| const fence = line.match(/^\s*(```|~~~)/); | |
| if (fence) { | |
| if (!inFence) { | |
| inFence = true; | |
| fenceMarker = fence[1]; | |
| } else if (fence[1] === fenceMarker) { | |
| inFence = false; | |
| fenceMarker = null; | |
| } | |
| continue; | |
| } | |
| if (inFence) { | |
| continue; | |
| } | |
| const heading = line.match(/^(#{1,6})\s+(.+?)\s*$/); | |
| if (!heading) { | |
| continue; | |
| } | |
| headings.push({ | |
| level: heading[1].length, | |
| title: heading[2].trim(), | |
| normalizedTitle: normalizeHeading(heading[2]), | |
| line: index + 1, | |
| lineIndex: index, | |
| }); | |
| } | |
| return headings.map((heading, index) => { | |
| const nextHeading = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level); | |
| const endLineIndex = nextHeading ? nextHeading.lineIndex : lines.length; | |
| return { | |
| ...heading, | |
| content: lines.slice(heading.lineIndex + 1, endLineIndex).join('\n'), | |
| }; | |
| }); | |
| } | |
| function findSection(sections, aliases) { | |
| const normalizedAliases = new Set(aliases.map(normalizeHeading)); | |
| return sections.find((section) => normalizedAliases.has(section.normalizedTitle)) || null; | |
| } | |
| function extractFencedCodeBlocks(content) { | |
| const lines = String(content || '').split(/\r?\n/); | |
| const blocks = []; | |
| let open = null; | |
| for (let index = 0; index < lines.length; index += 1) { | |
| const line = lines[index]; | |
| if (!open) { | |
| const start = line.match(/^\s*```([^\s`]*)?.*$/); | |
| if (start) { | |
| open = { | |
| lang: (start[1] || '').trim().toLowerCase(), | |
| startLine: index + 1, | |
| contentLines: [], | |
| }; | |
| } | |
| continue; | |
| } | |
| if (/^\s*```\s*$/.test(line)) { | |
| blocks.push({ | |
| lang: open.lang, | |
| startLine: open.startLine, | |
| content: open.contentLines.join('\n'), | |
| }); | |
| open = null; | |
| continue; | |
| } | |
| open.contentLines.push(line); | |
| } | |
| return blocks; | |
| } | |
| function textOutsideFencedCode(content) { | |
| const lines = String(content || '').split(/\r?\n/); | |
| const kept = []; | |
| let inFence = false; | |
| for (const line of lines) { | |
| if (/^\s*```/.test(line)) { | |
| inFence = !inFence; | |
| continue; | |
| } | |
| if (!inFence) { | |
| kept.push(line); | |
| } | |
| } | |
| return kept.join('\n'); | |
| } | |
| function normalizeTableCell(cell) { | |
| return String(cell || '') | |
| .replace(/`/g, '') | |
| .replace(/\*\*/g, '') | |
| .trim() | |
| .toLowerCase(); | |
| } | |
| function tableCells(line) { | |
| return line | |
| .trim() | |
| .replace(/^\|/, '') | |
| .replace(/\|$/, '') | |
| .split('|') | |
| .map(normalizeTableCell); | |
| } | |
| function isTableSeparator(line) { | |
| const cells = tableCells(line); | |
| return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell)); | |
| } | |
| function hasMarkdownTableWithColumns(content, columnAliasGroups) { | |
| const lines = textOutsideFencedCode(content).split(/\r?\n/); | |
| for (let index = 0; index < lines.length - 1; index += 1) { | |
| if (!lines[index].includes('|') || !lines[index + 1].includes('|') || !isTableSeparator(lines[index + 1])) { | |
| continue; | |
| } | |
| const headers = new Set(tableCells(lines[index]).filter(Boolean)); | |
| const hasColumns = columnAliasGroups.every((aliases) => | |
| aliases.map(normalizeTableCell).some((alias) => headers.has(alias)), | |
| ); | |
| if (hasColumns) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function filterFindings(findings, changedFiles) { | |
| if (!changedFiles || changedFiles.length === 0) { | |
| return findings; | |
| } | |
| const changed = new Set(changedFiles.map((filePath) => normalizePath(filePath))); | |
| return findings.filter((finding) => { | |
| const findingPath = normalizePath(finding.path || ''); | |
| const related = finding.related_paths || []; | |
| return ( | |
| changed.has(findingPath) || | |
| changed.has(findingPath.replace(/^\.\//, '')) || | |
| related.some((relatedPath) => changed.has(normalizePath(relatedPath))) | |
| ); | |
| }); | |
| } | |
| function readMarkdownEntry(rootDir, entry) { | |
| const absPath = path.join(rootDir, entry.source_path); | |
| if (!fs.existsSync(absPath)) { | |
| return null; | |
| } | |
| const raw = fs.readFileSync(absPath, 'utf8'); | |
| const parsed = matter(raw); | |
| return { | |
| raw, | |
| data: parsed.data || {}, | |
| content: parsed.content || '', | |
| }; | |
| } | |
| module.exports = { | |
| extractFencedCodeBlocks, | |
| filterFindings, | |
| findSection, | |
| hasMarkdownTableWithColumns, | |
| isLegacyVersion, | |
| makeFinding, | |
| normalizeHeading, | |
| parseMarkdownSections, | |
| readMarkdownEntry, | |
| severityForEntry, | |
| textOutsideFencedCode, | |
| }; |