156 lines
16 KiB
JavaScript
156 lines
16 KiB
JavaScript
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;
|
||
|
||
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(`<main class="static-main">${body}</main>`).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 `<ul class="static-list">${items.map(item=>`<li><a href="/${short}/${item.id}.html">${E(item.title)}</a><time datetime="${date(item.create_time)}">${date(item.create_time)}</time></li>`).join('')||'<li class="static-empty">暂无相关内容</li>'}</ul>`;}
|
||
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,`<h1>${title}</h1>${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,`<article class="static-article"><h1>${E(article.title||item.title)}</h1><div class="static-date">${date(item.create_time)}</div>${content('body').html()||''}<a class="static-back" href="/${short}.html">返回列表</a></article>`,`${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?`<nav class="static-pagination" aria-label="分页">${Array.from({length:pages},(_,i)=>i+1===n?`<span aria-current="page">${i+1}</span>`:`<a href="/${short}-classify-id-${category.id}${i?'-page-'+(i+1):''}.html">${i+1}</a>`).join('')}</nav>`:'';
|
||
shell(file,category.name,`<h1>${E(category.name)}</h1>${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,`<article class="static-article"><h1>${E(article.title||item.title)}</h1>${content('body').html()||''}<a class="static-back" href="/document.html">返回列表</a></article>`,`${site.origin}/plugin/26/helpTotal.htm?id=${item.id}`);
|
||
}
|
||
const docGroups=(data.api['/help']?.list||[]).map(group=>`<section><h2>${E(group.name)}</h2><ul class="static-list">${data.contents.help.filter(x=>String(x.categoryId)===String(group.id)).map(item=>`<li><a href="/docs/${item.id}.html">${E(item.title)}</a></li>`).join('')||'<li class="static-empty">暂无相关内容</li>'}</ul></section>`).join('');
|
||
shell('document.html','文档中心',`<h1>文档中心</h1><div class="input-search static-search"><input id="document-input" type="search" placeholder="搜索文档"><button class="search-btn" type="button">搜索</button></div>${docGroups}`);
|
||
shell('document-result.html','文档搜索',`<h1>文档搜索</h1><form class="static-search" id="static-search-form"><input id="static-search-query" name="search" type="search" placeholder="搜索文档"><button type="submit">搜索</button></form><ul class="static-list" id="static-search-results"></ul>`);
|
||
|
||
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=>`<tr data-filter-row><td>${E(country.name)} / ${E(city.name)}</td><td>${area.line.map(line=>E(line.name)).join(' / ')}</td></tr>`).join('')).join('')).join('');
|
||
const currency=data.api['/common']?.currency_code||'';
|
||
const price=product.price!==undefined?`<span data-no-translate>${E(currency)} ${E(product.price)}</span> / <span>${E(product.cycle)}</span>`:'联系咨询';
|
||
shell('cloud.html',product.name,`<h1>${E(product.name)}</h1><h2>公开起售价</h2><p class="static-price" data-no-translate>${price}</p><h2>可选配置</h2><div class="static-table-wrap"><table class="static-table"><tbody><tr><th>CPU(核)</th><td data-no-translate>${E(values(options.cpu))}</td></tr><tr><th>内存</th><td data-no-translate>${E(values(options.memory,options.config.memory_unit))}</td></tr><tr><th>系统盘</th><td data-no-translate>${E(values(options.system_disk,'GB'))}</td></tr><tr><th>数据盘</th><td data-no-translate>${E(values(options.data_disk,'GB'))}</td></tr></tbody></table></div><h2>地区与线路</h2><div class="static-search"><input type="search" data-local-filter placeholder="筛选地区或线路"></div><div class="static-table-wrap"><table class="static-table"><thead><tr><th>地区</th><th>线路</th></tr></thead><tbody>${regions}</tbody></table></div><p><a href="/contact.html">联系我们</a></p>`);
|
||
routes.set(canonical('/cart/goods.htm?id=4',site),'cloud.html');
|
||
const prepared=[];
|
||
for(const page of records.values()){
|
||
const $=cheerio.load(page.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($('<span class="static-availability"></span>').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=>`<div class="news-item"><a href="/${type==='announcement'?'announce':'news'}/${item.id}.html"><span class="news-text link-hover">${E(item.title)}</span><span class="news-time">${date(item.create_time)}</span></a></div>`).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=$('<a></a>').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(`<link rel="canonical" href="${relative(file,`${language}/${page.file}`)}">`);
|
||
for(const lang of languages)$('head').append(`<link rel="alternate" hreflang="${lang==='zh-hk'?'zh-Hant':lang==='zh-cn'?'zh-Hans':'en'}" href="${relative(file,`${lang}/${page.file}`)}">`);
|
||
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(`<link rel="stylesheet" href="${relative(file,'assets/site.css')}">`);
|
||
$('#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(`<script id="static-search-data" type="application/json">${JSON.stringify(searchData).replace(/</g,'\\u003c')}</script>`);
|
||
}
|
||
$('body').append(`<script src="${relative(file,'assets/site.js')}" defer></script>`);
|
||
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`);
|
||
}
|