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(/]*>([\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(/]*>([\s\S]*?)<\/tr>/gi)) { const cells = [...row[1].matchAll(/]*>([\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(/]*>([\s\S]*?)<\/body>/i)?.[1] || ''; body = body.replace(/]*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.`);