Files
bastion-frontend/build-main-card-data.mjs

163 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
const workspace = process.cwd();
const decoder = new TextDecoder('windows-1251');
const sources = [
{
scope: 'okb',
root: '/Users/anna/Desktop/Работа/данные для прототипа/Поисковый запрос 2',
main: 'АО _ОКБ МЭИ_.html',
parentKey: 'okb-mei',
extra: {
history: 'Особое конструкторское бюро/Доп.информация/АО _ОКБ МЭИ_(ист. справка).html',
specialization: 'Особое конструкторское бюро/Доп.информация/АО _ОКБ МЭИ_(специализация).html'
}
},
{
scope: 'saturn',
root: '/Users/anna/Desktop/Работа/данные для прототипа/Поисковый запрос 3',
main: 'ПАО _ОДК-Сатурн_.html',
parentKey: 'saturn',
extra: {
about: 'Сатурн/Доп.информация/ПАО _ОДК-Сатурн_(справка).html',
history: 'Сатурн/Доп.информация/ПАО _ОДК-Сатурн_(ист. справка).html',
specialization: 'Сатурн/Доп.информация/ПАО _ОДК-Сатурн_(специализация).html'
}
}
];
const walk = directory => fs.readdirSync(directory, {withFileTypes: true}).flatMap(entry => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? walk(target) : [target];
});
const decode = file => decoder.decode(fs.readFileSync(file));
const decodeEntities = value => String(value || '')
.replace(/ | /gi, ' ')
.replace(/"|"/gi, '"')
.replace(/&/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>');
const clean = value => decodeEntities(String(value || ''))
.replace(/<br\s*\/?\s*>/gi, ' ')
.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
const quoteName = value => clean(value).replace(/"([^"\n]+)"/g, '«$1»');
const sourceTitle = file => quoteName(decode(file).match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] || path.basename(file, '.html'));
const stableKey = (scope, relative) => `source-${scope}-${crypto.createHash('sha1').update(relative).digest('hex').slice(0, 10)}`;
function rowsFrom(file) {
const rows = [];
for (const row of decode(file).matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/gi)) {
const cells = [...row[1].matchAll(/<td[^>]*>([\s\S]*?)<\/td>/gi)].map(match => match[1]);
if (cells.length < 3) continue;
const label = clean(cells[0]);
const raw = cells.slice(2).join(' ');
const value = quoteName(raw);
if (label || value) rows.push({label, value, raw});
}
return rows;
}
function bodyText(file) {
let body = decode(file).replace(/[\r\n]+/g, ' ').match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] || '';
body = body.replace(/<p[^>]*align=["'](?:right|center)["'][\s\S]*?<\/p>/gi, ' ');
return clean(body);
}
function groupedRows(rows) {
const groups = Object.fromEntries(['ids', 'location', 'contacts', 'legal', 'finance', 'staff', 'management', 'ownership', 'structure', 'stability', 'other'].map(key => [key, []]));
let current = 'other';
const bucket = label => {
if (/^(id|ИНН|КПП|ОГРН|ОКПО|Код эмитента|ОКОПФ|ОКАТО|ОКТМО|ОКФС|ОКОГУ)$/i.test(label)) return 'ids';
if (/(Субъект федерации|Федеральный округ|Местонахождение|Юридический адрес|Почтовый адрес|Город)/i.test(label)) return 'location';
if (/(Телефоны|Факс|E-mail|Интернет-сайт|Раскрытие информации|Сайт|Википедия)/i.test(label)) return 'contacts';
if (/(Вид деятельности|ОКВЭД|Правовой статус|Действующие лицензии)/i.test(label)) return 'legal';
if (/(Выручка|Чистая прибыль|Убыт|Уставный капитал)/i.test(label)) return 'finance';
if (/(Численность|Среднесписоч)/i.test(label)) return 'staff';
if (/^(Руководитель|Должности)/i.test(label)) return 'management';
if (/(Держатель реестра|Номинал акции|Обыкновенных акций|Акционеры|Участие в капитале|Управляющая организация)/i.test(label)) return 'ownership';
if (/(присоединенные организации|филиалы|выделенные организации)/i.test(label)) return 'structure';
if (/(банкротств|несостоятельност|субсид)/i.test(label)) return 'stability';
return current;
};
for (const row of rows) {
if (row.label) current = bucket(row.label);
groups[current].push(row);
}
return groups;
}
const rowsText = rows => rows.map(row => `${row.label ? `${row.label}: ` : ''}${row.value}`).filter(Boolean).join('\n');
const firstValue = (rows, pattern) => rows.find(row => pattern.test(row.label))?.value || '';
const cleanPairs = rows => rows.filter(row => row.label && row.value).map(row => [row.label === 'id' ? 'ID' : row.label, row.value]);
function cardData(file, scope, relative) {
const rows = rowsFrom(file);
const groups = groupedRows(rows);
const title = sourceTitle(file);
const person = /^[А-ЯЁ][А-ЯЁа-яё-]+\s+[А-ЯЁ]\.[А-ЯЁ]\.$/.test(title) || /\/Руководители\//.test(`/${relative}`) && !/^(АО|ПАО|ООО|ОАО|ФГУП)\s/.test(title);
if (person) {
return {
title,
short: title,
type: 'person',
pairs: cleanPairs(groups.ids),
positions: groups.management.map(row => row.value).filter(Boolean),
sourcePath: relative
};
}
const pairs = cleanPairs([...groups.ids, ...groups.location, ...groups.contacts]);
const activityKind = firstValue(groups.legal, /^Вид деятельности$/i);
const okved = firstValue(groups.legal, /^ОКВЭД$/i);
const legalRows = groups.legal.filter(row => !/^(Вид деятельности|ОКВЭД)$/i.test(row.label));
const ownership = rowsText([...groups.ownership, ...groups.structure]);
return {
title,
short: title,
subtitle: relative.split('/').slice(0, -1).join(' · '),
pairs,
leadershipText: groups.management.map(row => row.value).filter(Boolean),
legal: rowsText(legalRows),
legalDetails: legalRows.map(row => `${row.label ? `${row.label}: ` : ''}${row.value}`),
activity: [activityKind, okved].filter(Boolean).join('. '),
ownership,
stability: groups.stability.length ? {bankruptcy: rowsText(groups.stability)} : undefined,
sourcePath: relative
};
}
const cards = {};
const titleIndex = {};
const mainExtras = {};
for (const config of sources) {
const htmlFiles = walk(config.root)
.filter(file => file.endsWith('.html'))
.filter(file => !file.includes(`${path.sep}Доп.информация${path.sep}`))
.sort((left, right) => left.localeCompare(right, 'ru'));
const mainFile = path.join(config.root, config.main);
const imported = htmlFiles.filter(file => file !== mainFile);
const keyByFile = new Map(imported.map(file => {
const relative = path.relative(config.root, file).split(path.sep).join('/');
return [file, stableKey(config.scope, relative)];
}));
for (const file of imported) {
const relative = path.relative(config.root, file).split(path.sep).join('/');
const key = keyByFile.get(file);
const data = cardData(file, config.scope, relative);
cards[key] = data;
(titleIndex[data.title] ||= []).push(key);
}
for (const [field, relative] of Object.entries(config.extra || {})) {
const file = path.join(config.root, relative);
if (fs.existsSync(file)) (mainExtras[config.parentKey] ||= {})[field] = bodyText(file);
}
}
const output = `/* Сгенерировано build-main-card-data.mjs из исходных HTML. */\nwindow.BASTION_IMPORTED_CARDS = ${JSON.stringify({cards, titleIndex, mainExtras}, null, 2)};\n`;
fs.writeFileSync(path.join(workspace, 'assets/js/main-card-data.js'), output);
console.log(`Generated ${Object.keys(cards).length} linked cards.`);