readme-ranker
Version:
A CLI tool to analyze and rank README files for projects, providing actionable suggestions for improvement.
331 lines (330 loc) • 13.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReadMeAnalyzer = void 0;
const fs = __importStar(require("fs-extra"));
class ReadMeAnalyzer {
constructor() {
this.sectionCriteria = [
{
name: 'Title',
regex: /^#\s.+/m,
score: 10,
suggestion: 'Add a title at the top using "# Project Name".'
},
{
name: 'Description',
regex: /##?\s*(Overview|Description)/i,
score: 10,
suggestion: 'Add a description section.'
},
{
name: 'Installation',
regex: /##?\s*Installation/i,
score: 10,
suggestion: 'Add an Installation section.'
},
{
name: 'Usage',
regex: /##?\s*Usage/i,
score: 10,
suggestion: 'Add a Usage section.'
},
{
name: 'Example',
regex: /##?\s*Example/i,
score: 5,
suggestion: 'Add an Example section.'
},
{
name: 'Contributing',
regex: /##?\s*Contributing/i,
score: 5,
suggestion: 'Add a Contributing section.'
},
{
name: 'License',
regex: /##?\s*License/i,
score: 10,
suggestion: 'Add a License section.'
},
{
name: 'Badges',
regex: /!\[.*\]\(.*\)/,
score: 5,
suggestion: 'Add badges (e.g., build, coverage, npm version).'
}
];
this.extraCriteria = [
{
name: 'Proper Title Format',
check: (content) => /^# [A-Z][\w\s\-]+$/m.test(content),
score: 3,
suggestion: 'Use a clear, properly capitalized project title.'
},
{
name: 'Description Length',
check: (content) => {
const match = content.match(/##?\s*(Overview|Description)[\s\S]*?(?=\n##? |\n# |\n$)/i);
return !!(match && match[0].split('\n').slice(1).join(' ').trim().length > 50);
},
score: 3,
suggestion: 'Provide a more detailed project description.'
},
{
name: 'Table of Contents',
check: (content) => /##?\s*Table of Contents/i.test(content),
score: 3,
suggestion: 'Add a Table of Contents section.'
},
{
name: 'Multiple Badges',
check: (content) => (content.match(/!\[.*\]\(.*\)/g) || []).length > 1,
score: 2,
suggestion: 'Add more badges (e.g., build, coverage, npm version).'
},
{
name: 'Images/Screenshots',
check: (content) => /!\[.*(screenshot|image|demo|preview).*?\]\(.*\)/i.test(content),
score: 3,
suggestion: 'Add images or screenshots to illustrate your project.'
},
{
name: 'Links',
check: (content) => /\[.*?\]\(https?:\/\/.*?\)/.test(content),
score: 2,
suggestion: 'Add relevant links (e.g., documentation, homepage, issues).'
},
{
name: 'Lists',
check: (content) => /^(\s*[-*+]|\d+\.)\s+/m.test(content),
score: 2,
suggestion: 'Use lists to organize information.'
},
{
name: 'Tables',
check: (content) => /\|.+\|.+\n\|[-\s|:]+\|/m.test(content),
score: 2,
suggestion: 'Add tables for structured data.'
},
{
name: 'Inline Code',
check: (content) => /`[^`]+`/.test(content),
score: 2,
suggestion: 'Use inline code for commands or filenames.'
},
{
name: 'Blockquotes',
check: (content) => /^>\s.+/m.test(content),
score: 1,
suggestion: 'Use blockquotes for tips or notes.'
},
{
name: 'Consistent Heading Levels',
check: (content) => {
const headings = (content.match(/^#+\s+/gm) || []).map(h => h.trim().length);
return headings.length > 1
? headings.every(h => h === headings[0] || h === headings[0] + 1)
: true;
},
score: 2,
suggestion: 'Use consistent heading levels for structure.'
},
{
name: 'No Placeholder Text',
check: (content) => !/(TODO|TBD|lorem ipsum|replace this)/i.test(content),
score: 2,
suggestion: 'Remove placeholder text like TODO, TBD, or lorem ipsum.'
},
{
name: 'No Broken Markdown',
check: (content) => !/(#+[^ \n])|(\[[^\]]*\]\([^)]+\s*$)/m.test(content),
score: 2,
suggestion: 'Fix broken Markdown syntax (headings, links, etc).'
},
{
name: 'Installation Section Has Code',
check: (content) => {
const match = content.match(/##?\s*Installation([\s\S]*?)(?=\n##? |\n# |\n$)/i);
return !!(match && /```/.test(match[1]));
},
score: 2,
suggestion: 'Add code blocks to the Installation section.'
},
{
name: 'Usage Section Has Code',
check: (content) => {
const match = content.match(/##?\s*Usage([\s\S]*?)(?=\n##? |\n# |\n$)/i);
return !!(match && /```/.test(match[1]));
},
score: 2,
suggestion: 'Add code blocks to the Usage section.'
},
{
name: 'Example Section Has Code',
check: (content) => {
const match = content.match(/##?\s*Example([\s\S]*?)(?=\n##? |\n# |\n$)/i);
return !!(match && /```/.test(match[1]));
},
score: 1,
suggestion: 'Add code blocks to the Example section.'
},
{
name: 'Contributing Section Has Guidelines',
check: (content) => {
const match = content.match(/##?\s*Contributing([\s\S]*?)(?=\n##? |\n# |\n$)/i);
return !!(match && /(pull request|issue|contribut|guideline|how to)/i.test(match[1]));
},
score: 1,
suggestion: 'Add contribution guidelines to the Contributing section.'
},
{
name: 'License Section Has License Name',
check: (content) => {
const match = content.match(/##?\s*License([\s\S]*?)(?=\n##? |\n# |\n$)/i);
return !!(match && /(mit|apache|gpl|bsd|mozilla|unlicense|lgpl|cc)/i.test(match[1]));
},
score: 1,
suggestion: 'Specify the license type in the License section.'
}
];
}
analyze(readmePath) {
return __awaiter(this, void 0, void 0, function* () {
const content = yield fs.readFile(readmePath, 'utf8');
const lines = content.split('\n');
let totalScore = 0;
let maxScore = 0;
const sections = {};
const extras = {};
const suggestions = [];
// Section checks
for (const crit of this.sectionCriteria) {
const present = crit.regex.test(content);
sections[crit.name] = {
present,
score: present ? crit.score : 0,
suggestion: present ? undefined : crit.suggestion
};
totalScore += present ? crit.score : 0;
maxScore += crit.score;
if (!present)
suggestions.push(crit.suggestion);
}
// Length check
let lengthScore = 0;
let lengthSuggestion;
if (lines.length < 10) {
lengthSuggestion = 'README is too short. Add more details.';
}
else if (lines.length > 200) {
lengthSuggestion = 'README is very long. Consider splitting into sections or separate docs.';
lengthScore = 5;
}
else {
lengthScore = 10;
}
totalScore += lengthScore;
maxScore += 10;
if (lengthSuggestion)
suggestions.push(lengthSuggestion);
// Formatting check
const headingCount = (content.match(/^#+\s+/gm) || []).length;
const codeBlockCount = (content.match(/```/g) || []).length / 2;
let formattingScore = 0;
let formattingSuggestion;
if (headingCount < 3) {
formattingSuggestion = 'Add more headings to organize your README.';
}
else if (codeBlockCount < 1) {
formattingSuggestion = 'Add code blocks for examples or usage instructions.';
}
else {
formattingScore = 10;
}
totalScore += formattingScore;
maxScore += 10;
if (formattingSuggestion)
suggestions.push(formattingSuggestion);
// Extra criteria
for (const crit of this.extraCriteria) {
let present = false;
try {
present = !!crit.check(content); // Ensure boolean
}
catch (_a) {
present = false;
}
extras[crit.name] = {
present,
score: present ? crit.score : 0,
suggestion: present ? undefined : crit.suggestion
};
totalScore += present ? crit.score : 0;
maxScore += crit.score;
if (!present && crit.suggestion)
suggestions.push(crit.suggestion);
}
return {
totalScore,
maxScore,
sections,
length: {
lines: lines.length,
score: lengthScore,
suggestion: lengthSuggestion
},
formatting: {
headings: headingCount,
codeBlocks: codeBlockCount,
score: formattingScore,
suggestion: formattingSuggestion
},
extras,
suggestions
};
});
}
}
exports.ReadMeAnalyzer = ReadMeAnalyzer;