import fs from 'node:fs/promises';
import path from 'node:path';
import * as cheerio from 'cheerio';
import { sites, languages } from './sites.mjs';
import { root, cacheRoot, jsonFile, write, writeJSON, canonical, routeOf, blockedPath, relative, escapeHTML as E } from './lib.mjs';
import { Assets } from './assets.mjs';
import { prepareTranslations, localize } from './translate.mjs';
import { addFonts } from './fonts.mjs';
const args=process.argv.slice(2);
const selected=sites.filter(s=>!args.some(a=>!a.startsWith('--'))||args.includes(s.id));
const date=t=>new Date(t*1000).toISOString().slice(0,10);
const detail=(item,type)=>item.detail?.[type]||item.detail||item;
const jskTelegram={url:'https://t.me/JSKCDN999',handle:'@JSKCDN999'};
for(const site of selected){
const folder=path.join(root,site.id);
const sourceFolder=path.join(cacheRoot,'sites',site.id);
const data=await jsonFile(path.join(sourceFolder,'data.json'));
const captured=await jsonFile(path.join(sourceFolder,'pages.json'),[]);
if(!captured.some(p=>p.file==='index.html'))throw new Error(`Capture ${site.id} first`);
const sourceReport=await jsonFile(path.join(sourceFolder,'capture-report.json'),{});
const asset=new Assets(site);
const records=new Map(captured.map(p=>[p.file,p]));
const routes=new Map(captured.map(p=>[canonical(p.url,site),p.file]));
const template=captured.find(p=>p.file==='document.html')||captured.find(p=>p.file==='index.html');
function shell(file,title,body,url){
const $=cheerio.load(template.html);const header=$('header').first().prop('outerHTML')||'';const footer=$('footer').first().prop('outerHTML')||$('body > .uk-section-primary').map((_,el)=>$(el).prop('outerHTML')).get().join('');
$('body').empty().append(header).append(`${body} `).append(footer);
$('title').text(`${title} - ${site.name}`);
const entry={file,url:url||site.origin+'/'+file,html:$.html(),title};records.set(file,entry);routes.set(canonical(entry.url,site),file);return entry;
}
function list(items,type){const short=type==='announcement'?'announce':type;return `
${items.map(item=>`${E(item.title)} ${date(item.create_time)} `).join('')||'暂无相关内容 '} `;}
for(const type of ['news','announcement']){
const short=type==='announcement'?'announce':type;const title=type==='announcement'?'官方公告':'新闻资讯';const items=data.contents[type];
shell(`${short}.html`,title,`${title} ${list(items,type)}`);
for(const item of items){
const article=detail(item,type);const content=cheerio.load(article.content||'');
shell(`${short}/${item.id}.html`,article.title||item.title,`${E(article.title||item.title)} ${date(item.create_time)}
${content('body').html()||''}返回列表 `,`${site.origin}/${short}-details.html?id=${item.id}`);
}
for(const category of data.api[`/${type}/type`]?.list||[]){
const filtered=items.filter(item=>String(detail(item,type)[`addon_idcsmart_${type}_type_id`])===String(category.id));
const pages=Math.max(1,Math.ceil(filtered.length/10));
for(let n=1;n<=pages;n++){
const file=`${short}-classify-id-${category.id}${n>1?'-page-'+n:''}.html`;
const pagination=pages>1?``:'';
shell(file,category.name,`${E(category.name)} ${list(filtered.slice((n-1)*10,n*10),type)}${pagination}`,`${site.origin}/${short}-classify.html?id=${category.id}${n>1?'&page='+n:''}`);
}
}
}
for(const item of data.contents.help){
const article=detail(item,'help');const content=cheerio.load(article.content||'');
content('span').each((_,el)=>{const $el=content(el);if(!$el.find('img,strong,b,i,em,a').length)$el.replaceWith($el.contents());});
shell(`docs/${item.id}.html`,article.title||item.title,`${E(article.title||item.title)} ${content('body').html()||''}返回列表 `,`${site.origin}/plugin/26/helpTotal.htm?id=${item.id}`);
}
const docGroups=(data.api['/help']?.list||[]).map(group=>`${E(group.name)} ${data.contents.help.filter(x=>String(x.categoryId)===String(group.id)).map(item=>`${E(item.title)} `).join('')||'暂无相关内容 '} `).join('');
shell('document.html','文档中心',`文档中心 搜索
${docGroups}`);
shell('document-result.html','文档搜索',`文档搜索 `);
if(site.id==='jskcloud'){
const contact=records.get('contact.html');
const $=cheerio.load(contact.html);
const telegramImage=$('.aside-tools .tools-item img').first().attr('src');
const documentImage=$('.support-box').filter((_,el)=>$(el).text().includes('自助文档')).find('img').attr('src');
$('.server-banner-desc').text('我们关怀每一位客户,致力于提供更高品质的服务。无论是产品咨询还是技术问题,欢迎通过 Telegram 联系 JSKCloud。');
$('.contact-support,.product-help').each((_,el)=>{
const previous=el.prev;
if(previous?.type==='text'&&!previous.data.trim())$(previous).remove();
$(el).remove();
});
$('.server-banner2').after(`
咨询时请说明所需产品、部署地区与配置,或描述遇到的问题,方便我们为您提供帮助。
`);
records.set(contact.file,{...contact,html:$.html()});
}
const product=data.api['/product/4']?.product;
const options=data.api['/product/4/mf_cloud/order_page'];
if(!product||!options)throw new Error(`${site.id}: public cloud product data missing`);
const values=(items,unit='')=>(items||[]).map(x=>x.type==='step'?`${x.min_value}–${x.max_value}${unit}`:x.value+unit).sort((a,b)=>parseFloat(a)-parseFloat(b)).join(' / ');
const regions=options.data_center.map(country=>country.city.map(city=>city.area.map(area=>`${E(country.name)} / ${E(city.name)} ${area.line.map(line=>E(line.name)).join(' / ')} `).join('')).join('')).join('');
const currency=data.api['/common']?.currency_code||'';
const price=product.price!==undefined?`${E(currency)} ${E(product.price)} / ${E(product.cycle)} `:'联系咨询';
shell('cloud.html',product.name,`${E(product.name)} 公开起售价 ${price}
可选配置 CPU(核) ${E(values(options.cpu))} 内存 ${E(values(options.memory,options.config.memory_unit))} 系统盘 ${E(values(options.system_disk,'GB'))} 数据盘 ${E(values(options.data_disk,'GB'))}
地区与线路
联系我们
`);
routes.set(canonical('/cart/goods.htm?id=4',site),'cloud.html');
const prepared=[];
for(const page of records.values()){
const $=cheerio.load(page.html);
if(site.id==='jskcloud'){
$('footer .footer-nav-right .footer-nav-box').html(``);
}
function stripComments(node){for(const child of [...node.children||[]]){if(child.type==='comment')$(child).remove();else stripComments(child);}}
stripComments($.root()[0]);
$('script').each((_,el)=>{const src=$(el).attr('src')||'';if(!/\/vender\/(?:jQuery|bootstrap|swiper|uikit)\//i.test(src))$(el).remove();});
$('base,noscript,iframe,object,embed,.no-login,.login-in,.login-menu,.header-clientarea,#loginBtn,#registBtn,#accountBtn,#financeBtn,#ticketBtn,#logout,.register-advert,.swiper-slide-duplicate').remove();
$('.js-buy,.add-cart').each((_,el)=>{const availability=$(el).find('.qty').text().trim();if(availability)$(el).replaceWith($(' ').text(availability));else $(el).remove();});
$('html').removeAttr('addons_js').removeAttr('id');
$('.static-price').removeAttr('data-no-translate');
$('*').each((_,el)=>{
for(const key of Object.keys(el.attribs||{}))if(/^on/i.test(key))$(el).removeAttr(key);
if($(el).attr('uk-scrollspy')){$(el).removeAttr('uk-scrollspy');const style=$(el).attr('style');if(style)$(el).attr('style',style.replace(/(?:visibility\s*:\s*hidden|opacity\s*:\s*0)\s*;?/gi,''));}
});
$('a[href]').each((_,el)=>{
const a=$(el);const href=(a.attr('href')||'').trim();
try{const u=new URL(href,page.url);if(site.hosts.includes(u.hostname)&&blockedPath(u.pathname)&&!/^\/cart\/goods\.htm$/.test(u.pathname)){if(a.closest('li').length)a.closest('li').remove();else a.remove();}}
catch{/* Relative fragment links need no normalization here. */}
if(/^mailto:\?@|^https?:\/\/t\.me\/\?/.test(href))a.remove();
});
$('a,button,.btn,.buy,.add-cart').each((_,el)=>{const text=$(el).text().trim();if(/^(?:立即购买|立即开通|立即创建|立即注册|免费试用|注册|登录|会员中心|控制台|Buy\s*now|Sign\s*up|Log\s*in|Client\s*Area|Create\s*now)$/i.test(text))$(el).remove();});
$('form').not('#static-search-form').each((_,el)=>$(el).replaceWith($(el).contents()));
$('a[href=""],a[href="javascript:;"],a[href="javascript:void(0)"]').each((_,el)=>{$(el).attr('href','#');});
if(page.file==='index.html'){
for(const [selector,type] of [['#newsList','news'],['#announceList','announcement']]){
$(selector).html(data.contents[type].slice(0,5).map(item=>``).join(''));
}
}
$('header .uk-parent').each((_,el)=>{const children=$(el).find('.uk-navbar-dropdown a');if($(el).find('.uk-navbar-dropdown').length&&!children.length)$(el).remove();});
$('img').each((_,el)=>{const img=$(el);if(!img.attr('src')&&!img.attr('data-src')&&!img.attr('srcset'))img.remove();});
prepared.push({...page,$,html:$.html()});
}
await write(path.join(folder,'assets','site.js'),await fs.readFile(path.join(root,'scripts','static','site.js')));
await write(path.join(folder,'assets','site.css'),await fs.readFile(path.join(root,'scripts','static','site.css')));
const variants=args.includes('--base-only')?['zh-cn']:languages;
if(!args.includes('--base-only'))await prepareTranslations(prepared.map(p=>p.$));
const output=[];
for(const page of prepared){
for(const language of variants){
const targets=[`${language}/${page.file}`];if(language===site.defaultLanguage||args.includes('--base-only'))targets.push(page.file);
for(const file of targets){
const $=cheerio.load(page.html);await localize($,language);
$('html').attr('lang',language==='en'?'en':language==='zh-hk'?'zh-Hant':'zh-Hans');$('body').attr('data-static-site',site.id);
$('a[href]').each((_,el)=>{
const a=$(el);const value=a.attr('href')?.trim();if(!value||/^(?:#|mailto:|tel:|javascript:|data:)/i.test(value))return;
let u;try{u=new URL(value,page.url);}catch{return;}
if(!site.hosts.includes(u.hostname)){a.attr('href',value.trim());if(a.attr('target')==='_blank')a.attr('rel','noopener noreferrer');return;}
const key=canonical(u.href,site);let target=routes.get(key);const mapped=routeOf(u.href,site);if(!target&&records.has(mapped))target=mapped;
if(!target){a.replaceWith(a.contents());return;}
a.attr('href',relative(file,`${language}/${target}`)+u.hash).removeAttr('target');
});
const labels={'zh-cn':'简体中文','zh-hk':'繁體中文','en':'English'};
$('.language-option').each((_,el)=>{const a=$(el);const code=({'en-us':'en','zh':'zh-cn','ftzh':'zh-hk'})[a.attr('data-lang')]||a.attr('data-lang');if(!languages.includes(code))return;const link=$(' ').attr('class',a.attr('class')||'language-option').attr('data-lang',code).attr('href',relative(file,`${code}/${page.file}`)).attr('lang',code).attr('hreflang',code);if(language===code)link.attr('aria-current','true');link.text(labels[code]);a.replaceWith(link);});
$('.language-code,.current-lang-label').text(labels[language]);
$('meta[http-equiv="Content-Language"]').attr('content',language);$('link[rel="canonical"],link[rel="alternate"][hreflang]').remove();
$('head').append(` `);
for(const lang of languages)$('head').append(` `);
for(const [selector,attr,hint] of [['img','src',''],['source','src',''],['video','src',''],['video','poster',''],['audio','src',''],['script[src]','src','.js'],['link[rel="stylesheet"]','href','.css'],['link[rel="icon"],link[rel="shortcut icon"]','href','']]){
$(selector).each((_,el)=>{const value=$(el).attr(attr);if(!value)return;const target=asset.ensure(value,page.url,hint);if(target)$(el).attr(attr,target.startsWith('data:')?target:relative(file,target));});
}
$('[srcset]').each((_,el)=>{const value=$(el).attr('srcset');if(value.startsWith('data:'))return;$(el).attr('srcset',value.split(',').map(part=>{const [url,...size]=part.trim().split(/\s+/);const target=asset.ensure(url,page.url);return [relative(file,target),...size].join(' ');}).join(', '));});
$('[data-src]').each((_,el)=>{const value=$(el).attr('data-src');if(!value||value.startsWith('#'))return;const target=asset.ensure(value,page.url);if(target)$(el).attr('data-src',target.startsWith('data:')?target:relative(file,target));});
$('style').each((_,el)=>$(el).text(asset.css($(el).text(),page.url,file)));
$('[style]').each((_,el)=>$(el).attr('style',asset.value($(el).attr('style'),page.url,file)));
$('link[rel="preconnect"],link[rel="dns-prefetch"],link[rel="prefetch"],link[rel="preload"]').remove();
$('head').append(` `);
$('#document-input').attr('data-results',relative(file,`${language}/document-result.html`));
if(page.file==='document-result.html'){
const searchItems=[];
for(const item of data.contents.help){const source=prepared.find(p=>p.file===`docs/${item.id}.html`);const t=cheerio.load(source.html);await localize(t,language);searchItems.push({title:t('.static-article h1').text(),text:t('.static-article').text(),href:relative(file,`${language}/docs/${item.id}.html`)});}
const searchData={items:searchItems,empty:language==='en'?'No results found.':language==='zh-hk'?'未找到相關內容':'未找到相关内容'};
$('body').append(``);
}
$('body').append(``);
await write(path.join(folder,file),$.html());output.push({file,language,source:page.url,title:$('title').text()});
}
}
console.log(`${site.id}: generated ${page.file} (${variants.length} languages)`);
}
await addFonts(asset,site,output);
await asset.settle();
const missingFonts=[...asset.records.values()].filter(entry=>entry.error&&/^https?:/.test(entry.url)&&/fonts\.googleapis\.com|fonts\.gstatic\.com/.test(new URL(entry.url).hostname));
if(missingFonts.length)throw new Error(`${site.id}: required offline fonts unavailable: ${missingFonts.map(entry=>entry.file).join(', ')}`);
await asset.prune(output.map(item=>item.file));
await writeJSON(path.join(folder,'site-manifest.json'),{site:site.id,origin:site.origin,capturedAt:data.capturedAt,generatedAt:new Date().toISOString(),defaultLanguage:site.defaultLanguage,languages:variants,pages:output,sourceFailures:sourceReport.failures||[],assetFailures:asset.failures,sourceMap:Object.fromEntries(routes)});
console.log(`${site.id}: ${output.length} HTML files, ${asset.records.size} resources, ${asset.failures.length} resource failures`);
}