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(/</gi, '<')
.replace(/>/gi, '>');
const clean = value => decodeEntities(String(value || ''))
.replace(/
/gi, ' ')
.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
const quoteName = value => clean(value).replace(/"([^"\n]+)"/g, '«$1»');
const sourceTitle = file => quoteName(decode(file).match(/
]*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.`);