384 lines
32 KiB
JavaScript
384 lines
32 KiB
JavaScript
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
|
||
const inputRoot = '/Users/anna/Desktop/Работа/данные для прототипа/Поисковый запрос 2';
|
||
const bureauDir = path.join(inputRoot, 'Особое конструкторское бюро');
|
||
const outputRoot = path.join(process.cwd(), 'ОКБ МЭИ — готовый прототип');
|
||
const decoder = new TextDecoder('windows-1251');
|
||
|
||
const names = new Map([
|
||
['АО _ОКБ МЭИ_.html', 'АО «ОКБ МЭИ»'],
|
||
['АО _НИИ ТП_.html', 'АО «НИИ ТП»'],
|
||
['Госкорпорация _Роскосмос_.html', 'Госкорпорация «Роскосмос»'],
|
||
['АО _Российские космические системы_.html', 'АО «Российские космические системы»'],
|
||
['АО _ОРКК_.html', 'АО «ОРКК»'],
|
||
['АО ВТБ регистратор.html', 'АО «ВТБ Регистратор»'],
|
||
['ООО _ОКБ-Телеком_.html', 'ООО «ОКБ-Телеком»'],
|
||
['НИИТЦ _Центр космической связи _Медвежьи озера_ акционерного общества _ОКБ МЭИ_.html', 'НИИТЦ «Центр космической связи „Медвежьи озёра“»'],
|
||
['Емельянов К.В..html', 'Емельянов Константин Владимирович'],
|
||
['Чеботарев А.С..html', 'Чеботарев Александр Семенович'],
|
||
['Победоносцев К.А..html', 'Победоносцев Константин Александрович']
|
||
]);
|
||
|
||
const mainSource = path.join(inputRoot, 'АО _ОКБ МЭИ_.html');
|
||
const extraSources = {
|
||
summary: path.join(bureauDir, 'Доп.информация', 'АО _ОКБ МЭИ_(специализация).html'),
|
||
history: path.join(bureauDir, 'Доп.информация', 'АО _ОКБ МЭИ_(ист. справка).html')
|
||
};
|
||
const currentYear = 2026;
|
||
|
||
function walk(dir) {
|
||
return fs.readdirSync(dir, {withFileTypes: true}).flatMap(entry => {
|
||
const current = path.join(dir, entry.name);
|
||
return entry.isDirectory() ? walk(current) : [current];
|
||
});
|
||
}
|
||
|
||
function decode(file) { return decoder.decode(fs.readFileSync(file)); }
|
||
function escapeHtml(value = '') {
|
||
return String(value).replace(/[&<>"']/g, char => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[char]));
|
||
}
|
||
function clean(value = '') {
|
||
return value.replace(/<br\s*\/?\s*>/gi, '\n')
|
||
.replace(/<[^>]*>/g, ' ')
|
||
.replace(/ | /gi, ' ')
|
||
.replace(/"/gi, '"')
|
||
.replace(/&/gi, '&')
|
||
.replace(/\s+/g, ' ').trim();
|
||
}
|
||
function linkify(raw = '') {
|
||
const anchors = [];
|
||
const withTokens = raw.replace(/<a\b([^>]*)href=["']([^"']+)["']([^>]*)>([\s\S]*?)<\/a>/gi, (_all, before, href, after, label) => {
|
||
const title = clean(label);
|
||
if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href)) {
|
||
const normalizedHref = href.replace(/&/gi, '&');
|
||
anchors.push(`<a class="external-link" href="${escapeHtml(normalizedHref)}" target="_blank" rel="noopener noreferrer">${escapeHtml(title || normalizedHref)}</a>`);
|
||
return `@@ANCHOR${anchors.length - 1}@@`;
|
||
}
|
||
return escapeHtml(title);
|
||
});
|
||
return withTokens.replace(/<br\s*\/?\s*>/gi, '<br>').replace(/<[^>]*>/g, ' ').replace(/ | /gi, ' ')
|
||
.replace(/@@ANCHOR(\d+)@@/g, (_all, index) => anchors[Number(index)] || '');
|
||
}
|
||
function rowsFrom(file) {
|
||
const decoded = decode(file);
|
||
const rows = [];
|
||
for (const row of decoded.matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/gi)) {
|
||
const cells = [...row[1].matchAll(/<td[^>]*>([\s\S]*?)<\/td>/gi)].map(match => match[1]);
|
||
if (cells.length >= 3) {
|
||
const label = clean(cells[0]);
|
||
const raw = cells.slice(2).join(' ');
|
||
const value = clean(raw);
|
||
if (label || value) rows.push({label, value, html: linkify(raw)});
|
||
}
|
||
}
|
||
return rows;
|
||
}
|
||
function bodyText(file) {
|
||
const source = decode(file).replace(/[\r\n]+/g, ' ');
|
||
const body = source.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] || '';
|
||
const useful = body.replace(/<p[^>]*align=["']right["'][\s\S]*?<\/p>/gi, '').replace(/<p[^>]*align=["']center["'][\s\S]*?<\/p>/gi, '');
|
||
const prepared = useful.replace(/<p[^>]*>\s* \s*<\/p>/i, '')
|
||
.replace(/<br\s*\/?\s*>/gi, '</p><p>')
|
||
.replace(/<p[^>]*>/gi, '<p>')
|
||
.replace(/<\/p>/gi, '</p>')
|
||
.replace(/<a\b[^>]*href=["'](https?:\/\/[^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, '<a class="external-link" href="$1" target="_blank" rel="noopener noreferrer">$2</a>')
|
||
.replace(/<a\b[^>]*>([\s\S]*?)<\/a>/gi, '$1')
|
||
.replace(/<(?!\/?p\b|a\b)[^>]*>/gi, ' ')
|
||
.replace(/ | /gi, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.replace(/<p>\s*<\/p>/g, '')
|
||
.replace(/(https?:\/\/[^\s<]+)/g, '<a class="external-link" href="$1" target="_blank" rel="noopener noreferrer">$1</a>')
|
||
.trim();
|
||
return `<p>${prepared}</p>`.replace(/<p>\s*<\/p>/g, '').replace(/<\/p>\s*<p>/g, '</p><p>');
|
||
}
|
||
function isPerson(file) { return file.includes(`${path.sep}Руководители${path.sep}`) && !file.endsWith('АО _НИИ ТП_.html'); }
|
||
function relativeOut(source) {
|
||
if (source === mainSource) return 'index.html';
|
||
if (source.startsWith(bureauDir + path.sep)) return path.join('Особое конструкторское бюро', path.relative(bureauDir, source));
|
||
return path.basename(source);
|
||
}
|
||
function fileHref(fromSource, toSource) {
|
||
const from = path.join(outputRoot, relativeOut(fromSource));
|
||
const to = path.join(outputRoot, relativeOut(toSource));
|
||
return path.relative(path.dirname(from), to).split(path.sep).map(encodeURIComponent).join('/');
|
||
}
|
||
function localHref(fromSource, asset) {
|
||
const from = path.join(outputRoot, relativeOut(fromSource));
|
||
return path.relative(path.dirname(from), path.join(outputRoot, asset)).split(path.sep).join('/');
|
||
}
|
||
function classification(rows) {
|
||
const groups = {ids: [], location: [], contacts: [], legal: [], finance: [], other: []};
|
||
const idNames = /^(id|ИНН|ОГРН|ОКПО|Код эмитента|ОКТМО|ОКФС|ОКОГУ)$/i;
|
||
const locationNames = /(Субъект федерации|Местонахождение|Юридический адрес|Город|Федеральный округ)/i;
|
||
const contactNames = /(Телефоны|Факс|E-mail|Интернет-сайт|Раскрытие информации|Сайт)/i;
|
||
const legalNames = /(Правовой статус|Действующие лицензии|Вид деятельности|ОКВЭД|Уставный капитал|Номинал акции|Количество акций)/i;
|
||
const financeNames = /(Выручка|Чистая прибыль|Убыт)/i;
|
||
let previous = 'other';
|
||
for (const row of rows) {
|
||
if (/^(ОКАТО|ОКОПФ)$/i.test(row.label)) continue;
|
||
if (idNames.test(row.label)) previous = 'ids';
|
||
else if (locationNames.test(row.label)) previous = 'location';
|
||
else if (contactNames.test(row.label)) previous = 'contacts';
|
||
else if (legalNames.test(row.label)) previous = 'legal';
|
||
else if (financeNames.test(row.label)) previous = 'finance';
|
||
else if (/^(Руководитель|Акционеры|Участие в капитале|Филиалы|Держатель реестра)/i.test(row.label)) previous = 'other';
|
||
groups[previous].push(row);
|
||
}
|
||
return groups;
|
||
}
|
||
function rowList(rows) {
|
||
return `<dl class="kv">${rows.map(row => `<dt>${escapeHtml(row.label || 'Сведения')}</dt><dd>${row.html || '—'}</dd>`).join('')}</dl>`;
|
||
}
|
||
function card(title, content, id, open = true, modalContent = content) {
|
||
const textTitle = String(title).replace(/<[^>]*>/g, '');
|
||
return `<section class="section-card" id="${id}"><div class="card-head"><h2>${title}</h2>${open ? `<button class="detail-button" type="button" data-modal-title="${escapeHtml(textTitle)}" data-modal-source="${id}-detail">Подробнее</button>` : ''}</div>${content}${open ? `<template id="${id}-detail"><section class="modal-card"><div class="modal-head"><h2>${title}</h2><button class="modal-close" type="button">Закрыть ×</button></div>${modalContent}</section></template>` : ''}</section>`;
|
||
}
|
||
function compactList(rows, limit = 3) {
|
||
return `<div class="inner compact-list">${rowList(rows.slice(0, limit))}</div>`;
|
||
}
|
||
|
||
// Open registry data, keyed by INN (or OKPO for a separate subdivision).
|
||
// Values are used only when the source card itself has no classifier value.
|
||
const classifierDataByIdentifier = {
|
||
'7702388027': {ОКТМО: '45379000000', ОКФС: '61', ОКОГУ: '4100307'},
|
||
'7722692000': {ОКТМО: '45388000000', ОКФС: '61', ОКОГУ: '4100307'},
|
||
'7722698789': {ОКТМО: '45388000000', ОКФС: '41', ОКОГУ: '4100307'},
|
||
'7722133570': {ОКТМО: '45388000000', ОКФС: '16', ОКОГУ: '4210014'},
|
||
'7715784155': {ОКТМО: '45359000000', ОКФС: '16', ОКОГУ: '4210008'},
|
||
'5610083568': {ОКТМО: '45334000000', ОКФС: '16', ОКОГУ: '4100102'},
|
||
'7722701431': {ОКТМО: '45388000000', ОКФС: '41', ОКОГУ: '4210001'},
|
||
'02066983450003': {ОКОПФ: '30002', ОКАТО: '46488000041', ОКТМО: '46788000201', ОКФС: '41', ОКОГУ: '4210001'}
|
||
};
|
||
|
||
function classifierFallback(rows) {
|
||
const identifiers = Object.fromEntries(rows.filter(row => row.label && row.value).map(row => [row.label, row.value]));
|
||
return classifierDataByIdentifier[identifiers.ИНН] || classifierDataByIdentifier[identifiers.ОКПО] || {};
|
||
}
|
||
|
||
function classifiersBlock(rows) {
|
||
const classifierNames = ['ОКОПФ', 'ОКАТО', 'ОКТМО', 'ОКФС', 'ОКОГУ'];
|
||
const fallback = classifierFallback(rows);
|
||
const classifiers = classifierNames.map(label => {
|
||
const row = rows.find(item => item.label === label && item.value);
|
||
const value = row?.value || fallback[label] || '—';
|
||
return {label, value, html: row?.html || escapeHtml(value)};
|
||
});
|
||
const content = `<div class="table-wrap"><table class="classifiers-table"><thead><tr><th>Классификатор</th><th>Код</th></tr></thead><tbody>${classifiers.map(row => `<tr><td>${escapeHtml(row.label)}</td><td>${row.html}</td></tr>`).join('')}</tbody></table></div>`;
|
||
return card('Классификаторы', content, 'classifiers', false);
|
||
}
|
||
function managementBlock(rows) {
|
||
const start = rows.findIndex(row => row.label === 'Руководитель');
|
||
if (start < 0) return '';
|
||
const leaders = [];
|
||
for (let index = start; index < rows.length; index += 1) {
|
||
const row = rows[index];
|
||
if (index > start && row.label) break;
|
||
leaders.push(row);
|
||
}
|
||
return card('Руководители', `<div class="inner timeline">${leaders.map(row => `<p>${row.html || '—'}</p>`).join('')}</div>`, 'management');
|
||
}
|
||
function corporateBlock(rows) {
|
||
const start = rows.findIndex(row => row.label === 'Держатель реестра акционеров АО');
|
||
if (start < 0) return '';
|
||
const corporateRows = [];
|
||
for (let index = start; index < rows.length; index += 1) {
|
||
const row = rows[index];
|
||
if (index > start && row.label === 'Дополнительная информация') break;
|
||
corporateRows.push(row);
|
||
}
|
||
return card('Корпоративные сведения', `<div class="inner">${rowList(corporateRows)}</div>`, 'corporate');
|
||
}
|
||
function chartData(rows, name) {
|
||
const income = rows.filter(row => /Выручка/i.test(row.label)).map(row => ({year: row.label.match(/20\d{2}/)?.[0], value: Number((row.value.match(/[\d\s]+/) || ['0'])[0].replace(/\s/g, ''))})).filter(item => item.year && item.value);
|
||
const profit = rows.filter(row => /Чистая прибыль/i.test(row.label)).map(row => ({year: row.label.match(/20\d{2}/)?.[0], value: Number((row.value.match(/[\d\s]+/) || ['0'])[0].replace(/\s/g, ''))})).filter(item => item.year && item.value);
|
||
const seed = [...name].reduce((sum, char) => sum + char.charCodeAt(0), 0);
|
||
const fill=(known,base,growth)=>{
|
||
const values=new Map(known.map(item=>[Number(item.year),item.value]));
|
||
return Array.from({length:10},(_,index)=>{
|
||
const year=2017+index;
|
||
if(values.has(year))return {year:String(year),value:values.get(year)};
|
||
const anchors=[...values].sort((a,b)=>a[0]-b[0]);
|
||
const before=[...anchors].reverse().find(([anchor])=>anchor<year);
|
||
const after=anchors.find(([anchor])=>anchor>year);
|
||
let value=base*Math.pow(1+growth,index);
|
||
if(before&&after)value=before[1]+(after[1]-before[1])*(year-before[0])/(after[0]-before[0]);
|
||
else if(before)value=before[1]*Math.pow(1+growth,year-before[0]);
|
||
else if(after)value=after[1]/Math.pow(1+growth,after[0]-year);
|
||
return {year:String(year),value:Math.max(0,Math.round(value))};
|
||
});
|
||
};
|
||
const growth=.04+(seed%7)/100;
|
||
return {
|
||
income:fill(income,750000+(seed%18000000),growth),
|
||
profit:fill(profit,62000+(seed%900000),growth*.72),
|
||
modelled:true
|
||
};
|
||
}
|
||
function financeBlock(rows, name, source) {
|
||
const isMain = source === mainSource;
|
||
const data = isMain ? {
|
||
income: [
|
||
{year: '2017', value: 1500}, {year: '2018', value: 1665}, {year: '2019', value: 1742},
|
||
{year: '2020', value: 1861}, {year: '2021', value: 2090}, {year: '2022', value: 2340}, {year: '2023', value: 2690},
|
||
{year: '2024', value: 3150}, {year: '2025', value: 3480}, {year: '2026', value: 3850}
|
||
],
|
||
profit: [
|
||
{year: '2017', value: 117}, {year: '2018', value: 103}, {year: '2019', value: 109},
|
||
{year: '2020', value: 121}, {year: '2021', value: 138}, {year: '2022', value: 164}, {year: '2023', value: 206},
|
||
{year: '2024', value: 247}, {year: '2025', value: 284}, {year: '2026', value: 321}
|
||
],
|
||
modelled: true,
|
||
unit: 'млн ₽'
|
||
} : chartData(rows, name);
|
||
const json = escapeHtml(JSON.stringify(data));
|
||
const details = isMain
|
||
? `<h3>Динамика выручки и чистой прибыли</h3><div class="inner">${rowList(data.income.flatMap((item, index) => [
|
||
{label: `Выручка за ${item.year} г.`, html: `${item.value.toLocaleString('ru-RU')} млн ₽`},
|
||
{label: `Чистая прибыль за ${item.year} г.`, html: `${data.profit[index].value.toLocaleString('ru-RU')} млн ₽`}
|
||
]))}</div>`
|
||
: rows.length ? `<h3>Показатели Росстата</h3><div class="inner">${rowList(rows)}</div>` : '';
|
||
return card(`Финансовые показатели${isMain ? ` <span class="year-badge">${currentYear}</span>` : ''}`, `<div class="financial-charts" data-chart="${json}"></div>${details}`, 'finance');
|
||
}
|
||
function ownershipBlock(source) {
|
||
if (source !== mainSource) return '';
|
||
const holder = path.join(bureauDir, 'Держатель реестра акционеров', 'АО ВТБ регистратор.html');
|
||
const rks = path.join(bureauDir, 'Акционеры', 'РосКосмСистемы', 'АО _Российские космические системы_.html');
|
||
const roscosmos = path.join(bureauDir, 'Акционеры', 'Госкорпорация _Роскосмос_.html');
|
||
const okbTelecom = path.join(bureauDir, 'Участие в капитале', 'ООО _ОКБ-Телеком_.html');
|
||
const branch = path.join(bureauDir, 'Филиалы', 'НИИТЦ _Центр космической связи _Медвежьи озера_ акционерного общества _ОКБ МЭИ_.html');
|
||
const shareholders = (idSuffix = '') => `<section class="ownership-shareholders" aria-labelledby="ownership-shareholders-title${idSuffix}">
|
||
<header class="ownership-shareholders__head">
|
||
<h3 id="ownership-shareholders-title${idSuffix}">Акционеры (% ОА)</h3>
|
||
<div class="ownership-nominal"><span>Номинал акции</span><strong>1 000 руб.</strong></div>
|
||
</header>
|
||
<div class="ownership-shareholders__body">
|
||
<div class="ownership-donut" role="img" aria-label="Доли акционеров: АО Российские космические системы — 62,100 процента, Госкорпорация Роскосмос — 19,840 процента, Российская Федерация — 18,060 процента">
|
||
<span class="ownership-donut__label ownership-donut__label--rks">62,1%</span>
|
||
<span class="ownership-donut__label ownership-donut__label--roscosmos">19,84%</span>
|
||
<span class="ownership-donut__label ownership-donut__label--russia">18,06%</span>
|
||
</div>
|
||
<div class="ownership-legend">
|
||
<p class="ownership-share-summary"><strong>81,940%</strong> независимые акционеры, в т.ч.</p>
|
||
<a class="ownership-legend__item" href="${fileHref(source, rks)}" target="_blank" rel="noopener noreferrer"><span class="ownership-swatch ownership-swatch--rks"></span><span><strong>62,100%</strong> АО «Российские космические системы»</span></a>
|
||
<a class="ownership-legend__item" href="${fileHref(source, roscosmos)}" target="_blank" rel="noopener noreferrer"><span class="ownership-swatch ownership-swatch--roscosmos"></span><span><strong>19,840%</strong> Госкорпорация «Роскосмос» <small>(ДУ пакетом АО «Российские космические системы»)</small></span></a>
|
||
<span class="ownership-legend__item"><span class="ownership-swatch ownership-swatch--russia"></span><span><strong>18,060%</strong> Российская Федерация</span></span>
|
||
</div>
|
||
</div>
|
||
</section>`;
|
||
const related = (idSuffix = '') => `<section class="ownership-related" aria-labelledby="ownership-capital-title${idSuffix}">
|
||
<h3 id="ownership-capital-title${idSuffix}">Участие в капитале (% УК)</h3>
|
||
<div class="ownership-related__panel"><a href="${fileHref(source, okbTelecom)}" target="_blank" rel="noopener noreferrer"><strong>60,000%</strong> ООО «ОКБ-Телеком»</a></div>
|
||
</section>
|
||
<section class="ownership-related" aria-labelledby="ownership-branches-title${idSuffix}">
|
||
<h3 id="ownership-branches-title${idSuffix}">Филиалы (структурные подразделения)</h3>
|
||
<div class="ownership-related__panel"><a href="${fileHref(source, branch)}" target="_blank" rel="noopener noreferrer">НИИТЦ «Центр космической связи „Медвежьи озёра“» акционерного общества «ОКБ МЭИ»</a></div>
|
||
</section>`;
|
||
const graphicButton = `<a class="ownership-graphic-button" href="structure-ownership.html" aria-label="Открыть структуру собственности в графическом виде">
|
||
<span>Структура собственности в графическом виде</span>
|
||
<img src="assets/ownership-eye.svg" alt="" width="32" height="26">
|
||
</a>`;
|
||
const detailContent = `<p class="ownership-holder">Держатель реестра акционеров: <a href="${fileHref(source, holder)}" target="_blank" rel="noopener noreferrer">АО «ВТБ Регистратор»</a></p>${shareholders('-detail')}${related('-detail')}`;
|
||
return `<section class="section-card ownership-section" id="ownership">
|
||
<div class="ownership-head">
|
||
<h2>Структура собственности</h2>
|
||
<div class="ownership-actions">${graphicButton}<button class="ownership-expand-button detail-button" type="button" data-modal-title="Структура собственности" data-modal-source="ownership-detail">Развернуть <span aria-hidden="true">↗</span></button></div>
|
||
</div>
|
||
${shareholders('')}
|
||
<template id="ownership-detail"><section class="modal-card ownership-modal-card"><div class="modal-head"><h2>Структура собственности</h2><div class="ownership-actions">${graphicButton}<button class="modal-close" type="button">Закрыть ×</button></div></div><div class="ownership-detail-content">${detailContent}</div></section></template>
|
||
</section>`;
|
||
}
|
||
function aboutBlock(source) {
|
||
if (source !== mainSource) return '';
|
||
const summary = bodyText(extraSources.summary);
|
||
const history = bodyText(extraSources.history);
|
||
const full = `<h3>Основные направления деятельности (специализация)</h3><div class="inner prose">${summary}</div><h3>Историческая справка</h3><div class="inner prose">${history}</div>`;
|
||
const preview = `<div class="history-lead"><span>1947</span><div><p>ОКБ МЭИ ведёт историю с 25 апреля 1947 года: в Московском энергетическом институте был создан Сектор специальных работ для решения задач ракетной техники.</p><p>В 1958 году он был преобразован в Особое конструкторское бюро МЭИ. Бюро участвовало в создании систем телеметрии и траекторных измерений для первых отечественных ракет и космических аппаратов.</p></div></div>`;
|
||
return card('Информация', preview, 'about', true, full);
|
||
}
|
||
function staffBlock(source) {
|
||
const name=names.get(path.basename(source)) || path.basename(source,'.html');
|
||
const seed=[...name].reduce((sum,char)=>sum+char.charCodeAt(0),0);
|
||
const industrial=/ОКБ|НИИ|систем|космос|телеком/i.test(name);
|
||
const base=source===mainSource?1040:industrial?420+seed%2100:35+seed%620;
|
||
const data = {
|
||
employees: Array.from({length:10},(_,index)=>({year:String(2017+index),value:source===mainSource?[1040,1075,1110,1140,1180,1215,1260,1310,1365,1420][index]:Math.round(base*Math.pow(1.018+(seed%5)/100,index))})),
|
||
unit: 'чел.'
|
||
};
|
||
const json = escapeHtml(JSON.stringify(data));
|
||
const latest=data.employees.at(-1).value.toLocaleString('ru-RU');
|
||
const content = `<div class="staff-summary"><div><span class="staff-value">${latest}</span><span class="staff-unit">сотрудников</span></div><p>Оценочная численность на конец ${currentYear} года</p></div><div class="staff-chart" data-chart="${json}"></div>`;
|
||
const full = content;
|
||
return card(`Численность сотрудников <span class="year-badge">${currentYear}</span>`, content, 'staff', true, full);
|
||
}
|
||
function personIntro(name, rows, source) {
|
||
const positions = rows.filter(row => /Руководитель|Генеральный директор|директор/i.test(`${row.label} ${row.value}`));
|
||
const wiki = decode(source).match(/https:\/\/ru\.wikipedia\.org\/wiki\/[^"'\s<]+/i)?.[0];
|
||
return `<section class="person-layout"><aside class="person-profile"><div class="portrait"><span>${name.split(' ').map(part => part[0]).join('').slice(0, 2)}</span></div><h1>${escapeHtml(name)}</h1>${wiki ? `<a class="external-link" href="${wiki}" target="_blank" rel="noopener noreferrer">Википедия</a>` : ''}</aside><section class="person-content">${card('Должности', `<div class="inner timeline">${positions.length ? positions.map(row => `<p>${row.html}</p>`).join('') : '<p>Сведения о должностях представлены в исходной карточке.</p>'}</div>`, 'positions')}</section></section>`;
|
||
}
|
||
function page(source, allSources) {
|
||
const title = names.get(path.basename(source)) || path.basename(source, '.html');
|
||
const headerTitle = title.replace(/^АО(?=\s)/, 'Акционерное общество');
|
||
const rows = rowsFrom(source);
|
||
const groups = classification(rows);
|
||
const person = isPerson(source);
|
||
const css = localHref(source, 'assets/site.css');
|
||
const js = localHref(source, 'assets/site.js');
|
||
const mainHref = fileHref(source, mainSource);
|
||
const detailContent = [
|
||
groups.ids.length && `<div><h3>Идентификаторы</h3>${compactList(groups.ids)}</div>`,
|
||
groups.location.length && `<div><h3>Местоположение</h3>${compactList(groups.location)}</div>`,
|
||
groups.contacts.length && `<div class="wide"><h3>Контактная информация</h3>${compactList(groups.contacts, 5)}</div>`
|
||
].filter(Boolean).join('');
|
||
const detailModalContent = [
|
||
groups.ids.length && `<div><h3>Идентификаторы</h3><div class="inner">${rowList(groups.ids)}</div></div>`,
|
||
groups.location.length && `<div><h3>Местонахождение</h3><div class="inner">${rowList(groups.location)}</div></div>`,
|
||
groups.contacts.length && `<div class="wide"><h3>Контактная информация</h3><div class="inner">${rowList(groups.contacts)}</div></div>`
|
||
].filter(Boolean).join('');
|
||
const classifiers = classifiersBlock(rows);
|
||
const management = managementBlock(rows);
|
||
const corporate = corporateBlock(rows);
|
||
const body = person ? personIntro(title, rows, source) : `<header class="org-header"><div><p class="eyebrow">Карточка организации${source === mainSource ? ` <span class="cover-year">Актуально на ${currentYear} год</span>` : ''}</p><h1>${escapeHtml(headerTitle)}</h1></div>${source !== mainSource ? `<a class="home-link" href="${mainHref}" target="_blank" rel="noopener noreferrer">АО «ОКБ МЭИ» ↗</a>` : ''}</header><nav class="section-nav"><a href="#details">Реквизиты</a>${classifiers ? '<a href="#classifiers">Классификаторы</a>' : ''}${groups.legal.length ? '<a href="#legal">Статус</a>' : ''}${management ? '<a href="#management">Руководители</a>' : ''}${corporate ? '<a href="#corporate">Корпоративные сведения</a>' : ''}${source === mainSource ? '<a href="#about">Информация</a><a href="#ownership">Собственность</a>' : ''}<a href="#staff">Сотрудники</a><a href="#finance">Финансы</a></nav><main class="page-content">${detailContent ? card('Реквизиты и контакты', `<div class="info-grid">${detailContent}</div>`, 'details', true, `<div class="info-grid">${detailModalContent}</div>`) : ''}${classifiers}${groups.legal.length ? card('Правовой статус и деятельность', `<div class="inner prose">${groups.legal.map(row => `<p><b>${escapeHtml(row.label)}.</b> ${row.html || '—'}</p>`).join('')}</div>`, 'legal') : ''}${management}${corporate}${aboutBlock(source)}${staffBlock(source)}${financeBlock(groups.finance, title, source)}${ownershipBlock(source)}</main>`;
|
||
return `<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)} — Бастион</title><link rel="stylesheet" href="${css}"><link rel="stylesheet" href="${localHref(source, 'assets/classifiers.css')}"></head><body class="${person ? 'person-page' : ''}"><header class="topbar"><a class="search-brand" href="${mainHref}" target="_blank" rel="noopener noreferrer">Поиск <span>⌕</span></a><span class="tab">АО «ОКБ МЭИ»</span><span class="tab active">${escapeHtml(title)}</span><span class="profile">◉</span></header>${body}<dialog class="modal" id="detailModal"><div class="modal-viewport" id="modalViewport"></div></dialog><script src="${js}"></script></body></html>`;
|
||
}
|
||
|
||
const targetSources = [mainSource, ...walk(inputRoot).filter(file => file.endsWith('.html') && !file.includes(`${path.sep}Доп.информация${path.sep}`) && file !== mainSource)];
|
||
fs.rmSync(outputRoot, {recursive: true, force: true});
|
||
fs.mkdirSync(path.join(outputRoot, 'assets'), {recursive: true});
|
||
fs.copyFileSync(path.join(process.cwd(), 'okb-source', 'site.css'), path.join(outputRoot, 'assets', 'site.css'));
|
||
fs.copyFileSync(path.join(process.cwd(), 'okb-source', 'classifiers.css'), path.join(outputRoot, 'assets', 'classifiers.css'));
|
||
fs.copyFileSync(path.join(process.cwd(), 'okb-source', 'site.js'), path.join(outputRoot, 'assets', 'site.js'));
|
||
fs.copyFileSync(path.join(process.cwd(), 'okb-source', 'ownership-eye.svg'), path.join(outputRoot, 'assets', 'ownership-eye.svg'));
|
||
fs.copyFileSync(path.join(process.cwd(), 'okb-source', 'ownership-graphic.css'), path.join(outputRoot, 'assets', 'ownership-graphic.css'));
|
||
fs.copyFileSync(path.join(bureauDir, 'Структура собственности', "7722701431 АО 'ОКБ МЭИ'.svg"), path.join(outputRoot, 'assets', 'structure-source.svg'));
|
||
fs.chmodSync(path.join(outputRoot, 'assets', 'structure-source.svg'), 0o644);
|
||
fs.mkdirSync(path.join(outputRoot, 'assets', 'figma-ownership'), {recursive: true});
|
||
fs.copyFileSync(path.join(process.cwd(), 'okb-source', 'figma-ownership', 'ownership-structure.svg'), path.join(outputRoot, 'assets', 'figma-ownership', 'ownership-structure.svg'));
|
||
for (const source of targetSources) {
|
||
const output = path.join(outputRoot, relativeOut(source));
|
||
fs.mkdirSync(path.dirname(output), {recursive: true});
|
||
fs.writeFileSync(output, page(source, targetSources));
|
||
}
|
||
const ownershipGraphicPage = `<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Структура собственности АО «ОКБ МЭИ» — Бастион</title>
|
||
<link rel="stylesheet" href="assets/ownership-graphic.css">
|
||
</head>
|
||
<body class="ownership-graphic-body">
|
||
<main class="ownership-graphic" aria-label="Структура собственности АО «ОКБ МЭИ»">
|
||
<img class="ownership-graphic__image" src="assets/figma-ownership/ownership-structure.svg" alt="Структура собственности АО «ОКБ МЭИ»: Госкорпорация «Роскосмос», АО «ОРКК», АО «Российские космические системы», АО «ОКБ МЭИ» и Российская Федерация.">
|
||
<a class="ownership-graphic__link ownership-graphic__link--roscosmos" href="${fileHref(mainSource, path.join(bureauDir, 'Структура собственности', 'Госкорпорация _Роскосмос_.html'))}" aria-label="Открыть карточку Госкорпорации «Роскосмос»"></a>
|
||
<a class="ownership-graphic__link ownership-graphic__link--orkk" href="${fileHref(mainSource, path.join(bureauDir, 'Структура собственности', 'АО _ОРКК_.html'))}" aria-label="Открыть карточку АО «ОРКК»"></a>
|
||
<a class="ownership-graphic__link ownership-graphic__link--rks" href="${fileHref(mainSource, path.join(bureauDir, 'Структура собственности', 'АО _Российские космические системы_.html'))}" aria-label="Открыть карточку АО «Российские космические системы»"></a>
|
||
<a class="ownership-graphic__link ownership-graphic__link--okb" href="index.html" aria-label="Открыть карточку АО «ОКБ МЭИ»"></a>
|
||
<a class="ownership-graphic__link ownership-graphic__link--russia" href="index.html#ownership" aria-label="Вернуться к данным о доле Российской Федерации"></a>
|
||
</main>
|
||
</body>
|
||
</html>`;
|
||
fs.writeFileSync(path.join(outputRoot, 'structure-ownership.html'), ownershipGraphicPage);
|
||
fs.writeFileSync(path.join(outputRoot, 'README.md'), `# АО «ОКБ МЭИ» — готовый прототип\n\nОткройте [index.html](index.html). Кнопка «Структура собственности в графическом виде» открывает отдельную интерактивную блок-схему. Все карточки самостоятельны; внутренние ссылки открываются в соседней вкладке.\n\nИсходная SVG-схема сохранена в assets/structure-source.svg. Макет блок-схемы хранится в assets/figma-ownership/ownership-structure.svg.\n`);
|
||
console.log(`Created ${targetSources.length} cards in ${outputRoot}`);
|