raise editorial quality gate and revise 2018 spring
Build and deploy / deploy (push) Successful in 15s
Build and deploy / deploy (push) Successful in 15s
This commit is contained in:
@@ -1,21 +1,38 @@
|
||||
import { access, readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { editorialRevisions } from '../data/editorial-revisions.mjs';
|
||||
|
||||
const webRoot = join(fileURLToPath(new URL('..', import.meta.url)));
|
||||
const articlesPath = join(webRoot, 'data', 'articles.json');
|
||||
const slugs = process.argv.slice(2);
|
||||
const archivedArticles = JSON.parse(await readFile(articlesPath, 'utf8'));
|
||||
const revisionBySlug = new Map(
|
||||
editorialRevisions.map((revision) => [revision.slug, revision]),
|
||||
);
|
||||
const archive = archivedArticles.map((article) => ({
|
||||
...article,
|
||||
...revisionBySlug.get(article.slug),
|
||||
}));
|
||||
const requestedSlugs = process.argv.slice(2);
|
||||
const slugs = requestedSlugs.includes('--all-editorial')
|
||||
? archive.filter((article) => article.slug.startsWith('editorial-')).map((article) => article.slug)
|
||||
: requestedSlugs;
|
||||
|
||||
if (slugs.length === 0) {
|
||||
throw new Error('Usage: node scripts/audit-quality-batch.mjs <article-slug> [...slug]');
|
||||
throw new Error('Usage: node scripts/audit-quality-batch.mjs <article-slug> [...slug] | --all-editorial');
|
||||
}
|
||||
|
||||
const archive = JSON.parse(await readFile(articlesPath, 'utf8'));
|
||||
const genericPhrases = [
|
||||
'У этой модели нет магической силы',
|
||||
'Материалы для проверки',
|
||||
'Если держать этот порядок, решение остаётся понятным',
|
||||
'В современном мире',
|
||||
'очень важно',
|
||||
'следует отметить',
|
||||
'просто нужно',
|
||||
'нужно понимать, что',
|
||||
];
|
||||
const MIN_BODY_CHARS = 5000;
|
||||
const MAX_BODY_CHARS = 15000;
|
||||
let failed = false;
|
||||
|
||||
function count(content, expression) {
|
||||
@@ -30,6 +47,12 @@ function plainText(content) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function bodyText(content) {
|
||||
return plainText(
|
||||
content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''),
|
||||
);
|
||||
}
|
||||
|
||||
for (const slug of slugs) {
|
||||
const article = archive.find((candidate) => candidate.slug === slug);
|
||||
const issues = [];
|
||||
@@ -41,19 +64,43 @@ for (const slug of slugs) {
|
||||
}
|
||||
|
||||
const content = article.contentHtml;
|
||||
const text = plainText(content);
|
||||
const body = bodyText(content);
|
||||
const imageSources = [...content.matchAll(/<img[^>]+src="([^"]+)"/g)].map((match) => match[1]);
|
||||
const figures = [...content.matchAll(/<figure>([\s\S]*?)<\/figure>/g)].map((match) => match[1]);
|
||||
|
||||
if (article.readingMinutes < 8) issues.push('указано меньше 8 минут чтения');
|
||||
if (text.length < 4800) issues.push('меньше 4800 символов осмысленного текста');
|
||||
if (body.length < MIN_BODY_CHARS) {
|
||||
issues.push('меньше ' + MIN_BODY_CHARS + ' знаков основного текста');
|
||||
}
|
||||
if (body.length > MAX_BODY_CHARS) {
|
||||
issues.push('больше ' + MAX_BODY_CHARS + ' знаков основного текста');
|
||||
}
|
||||
if (count(content, /<h2>/g) < 5) issues.push('меньше пяти смысловых разделов');
|
||||
if (count(content, /<figure>/g) < 1 || imageSources.length < 1) issues.push('нет визуального объяснения');
|
||||
if (count(content, /<figcaption>/g) < 1) issues.push('у иллюстрации нет подписи');
|
||||
if (count(content, /<table>/g) < 1 || count(content, /<thead>/g) < 1) issues.push('нет доступной таблицы');
|
||||
if (count(content, /<pre><code>/g) < 1) issues.push('нет воспроизводимого примера');
|
||||
if (count(content, /<ol>/g) < 1) issues.push('нет последовательности проверки или действий');
|
||||
if (count(content, /<a href="https?:\/\//g) < 2) issues.push('меньше двух внешних источников');
|
||||
if (!content.includes('<h2>Проверяемые источники</h2>')) issues.push('нет отдельного раздела с источниками');
|
||||
if (content.includes('undefined') || content.includes('[object Object]')) issues.push('в тексте есть след генерации');
|
||||
const proseWithoutCode = content
|
||||
.replace(/<pre><code>[\s\S]*?<\/code><\/pre>/g, '')
|
||||
.replace(/<code>[\s\S]*?<\/code>/g, '');
|
||||
if (proseWithoutCode.includes('undefined') || proseWithoutCode.includes('[object Object]')) {
|
||||
issues.push('в тексте есть след генерации');
|
||||
}
|
||||
if (!/(проблем|ошиб|симптом|сбой|задач)/i.test(body.slice(0, 800))) {
|
||||
issues.push('проблема не названа в начале текста');
|
||||
}
|
||||
|
||||
for (const figure of figures) {
|
||||
const image = figure.match(/<img\b[^>]*>/);
|
||||
const alt = image?.[0].match(/\balt="([^"]*)"/);
|
||||
if (!image || !alt || alt[1].trim() === '') {
|
||||
issues.push('у рисунка нет содержательного alt-текста');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of genericPhrases) {
|
||||
if (content.includes(phrase)) issues.push('обнаружен шаблонный оборот: «' + phrase + '»');
|
||||
@@ -73,7 +120,7 @@ for (const slug of slugs) {
|
||||
} else {
|
||||
console.log(
|
||||
'PASS ' + slug
|
||||
+ ': ' + text.length + ' chars, '
|
||||
+ ': ' + body.length + ' body chars, '
|
||||
+ count(content, /<figure>/g) + ' figure, '
|
||||
+ count(content, /<table>/g) + ' table, '
|
||||
+ count(content, /<pre><code>/g) + ' code example',
|
||||
|
||||
Reference in New Issue
Block a user