553 lines
20 KiB
JavaScript
Executable File
553 lines
20 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
|
const distDir = path.join(root, 'dist');
|
|
const wwwDir = path.join(distDir, 'www');
|
|
const h5Dir = path.join(distDir, 'h5');
|
|
|
|
const siteContent = readJson(path.join(root, 'content', 'site.json'));
|
|
const legalContent = readJson(path.join(root, 'content', 'legal.json'));
|
|
const H5_PRIVACY_URL = `${siteContent.company.h5Website}/privacy.html`;
|
|
const H5_TERMS_URL = `${siteContent.company.h5Website}/terms.html`;
|
|
const H5_DELETE_ACCOUNT_URL = `${siteContent.company.h5Website}/delete-account.html`;
|
|
const H5_CHILD_SAFETY_URL = `${siteContent.company.h5Website}/child-safety.html`;
|
|
const landingPages = fs
|
|
.readdirSync(path.join(root, 'content', 'landing-pages'))
|
|
.filter((file) => file.endsWith('.json'))
|
|
.map((file) => readJson(path.join(root, 'content', 'landing-pages', file)))
|
|
.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
|
|
build();
|
|
|
|
function build() {
|
|
fs.rmSync(distDir, { force: true, recursive: true });
|
|
ensureDir(path.join(wwwDir, 'assets'));
|
|
ensureDir(path.join(h5Dir, 'assets'));
|
|
copyFile(path.join(root, 'src', 'site.css'), path.join(wwwDir, 'assets', 'site.css'));
|
|
copyFile(path.join(root, 'src', 'site.css'), path.join(h5Dir, 'assets', 'site.css'));
|
|
|
|
writeHtml(path.join(wwwDir, 'index.html'), renderHomePage());
|
|
writeHtml(
|
|
path.join(wwwDir, 'privacy', 'index.html'),
|
|
renderRedirectPage({
|
|
title: 'Privacy Policy Redirect',
|
|
target: H5_PRIVACY_URL,
|
|
}),
|
|
);
|
|
writeHtml(
|
|
path.join(wwwDir, 'terms', 'index.html'),
|
|
renderRedirectPage({
|
|
title: 'Terms of Service Redirect',
|
|
target: H5_TERMS_URL,
|
|
}),
|
|
);
|
|
writeHtml(
|
|
path.join(wwwDir, 'delete-account', 'index.html'),
|
|
renderRedirectPage({
|
|
title: 'Delete Account Redirect',
|
|
target: H5_DELETE_ACCOUNT_URL,
|
|
}),
|
|
);
|
|
writeHtml(
|
|
path.join(wwwDir, 'child-safety', 'index.html'),
|
|
renderRedirectPage({
|
|
title: 'Child Safety Standards Redirect',
|
|
target: H5_CHILD_SAFETY_URL,
|
|
}),
|
|
);
|
|
writeText(path.join(wwwDir, 'robots.txt'), 'User-agent: *\nAllow: /\n');
|
|
writeText(path.join(wwwDir, 'sitemap.xml'), renderSiteMap());
|
|
|
|
writeHtml(path.join(h5Dir, 'index.html'), renderLandingIndexPage());
|
|
writeHtml(
|
|
path.join(h5Dir, 'privacy.html'),
|
|
renderPlainLegalPage('privacy', {
|
|
canonical: H5_PRIVACY_URL,
|
|
}),
|
|
);
|
|
writeHtml(
|
|
path.join(h5Dir, 'terms.html'),
|
|
renderLegalPage('terms', {
|
|
canonical: H5_TERMS_URL,
|
|
homeHref: '/',
|
|
privacyHref: '/privacy.html',
|
|
termsHref: '/terms.html',
|
|
primaryActionHref: 'https://www.haiyihy.com/',
|
|
primaryActionLabel: 'Official Site',
|
|
}),
|
|
);
|
|
writeHtml(
|
|
path.join(h5Dir, 'delete-account.html'),
|
|
renderPlainLegalPage('deleteAccount', {
|
|
canonical: H5_DELETE_ACCOUNT_URL,
|
|
}),
|
|
);
|
|
writeHtml(
|
|
path.join(h5Dir, 'child-safety.html'),
|
|
renderPlainLegalPage('childSafety', {
|
|
canonical: H5_CHILD_SAFETY_URL,
|
|
}),
|
|
);
|
|
writeHtml(path.join(h5Dir, 'studio', 'index.html'), renderStudioPage());
|
|
writeText(path.join(h5Dir, 'data', 'pages.json'), `${JSON.stringify(landingPages, null, 2)}\n`);
|
|
|
|
for (const page of landingPages) {
|
|
writeHtml(path.join(h5Dir, page.slug, 'index.html'), renderLandingPage(page));
|
|
}
|
|
|
|
console.log(`Built public site to ${distDir}`);
|
|
}
|
|
|
|
function renderHomePage() {
|
|
const { brand, company, hero } = siteContent;
|
|
return renderDocument({
|
|
title: `${brand.name} | Official Website`,
|
|
description: brand.description,
|
|
canonical: `${company.website}/`,
|
|
hostRoot: '',
|
|
theme: 'ember',
|
|
bodyClass: 'corp-body',
|
|
body: `
|
|
<div class="corp-home">
|
|
<header class="corp-header">
|
|
<div class="shell corp-header__inner">
|
|
<a class="corp-brand" href="/">
|
|
<span class="corp-brand__mark" aria-hidden="true"></span>
|
|
<span class="corp-brand__text">
|
|
<strong>${escapeHtml(brand.name)}</strong>
|
|
<span>${escapeHtml(company.industry)}</span>
|
|
</span>
|
|
</a>
|
|
<nav class="corp-nav" aria-label="Primary">
|
|
<a href="#about">About Us</a>
|
|
<a href="${escapeAttr(H5_PRIVACY_URL)}">Privacy</a>
|
|
<a href="${escapeAttr(H5_TERMS_URL)}">Terms</a>
|
|
<a href="${escapeAttr(company.h5Website)}">H5 Pages</a>
|
|
<a href="#contact">Contact</a>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="corp-main">
|
|
<section class="corp-hero">
|
|
<div class="corp-hero__overlay"></div>
|
|
<div class="corp-hero__image"></div>
|
|
<div class="shell corp-hero__content">
|
|
<div class="corp-hero__eyebrow">${escapeHtml(hero.eyebrow)}</div>
|
|
<h1 class="corp-hero__title">${escapeHtml(hero.title)}</h1>
|
|
<p class="corp-hero__summary">${escapeHtml(hero.summary)}</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="corp-strip" id="about">
|
|
<div class="shell corp-strip__grid">
|
|
<article class="corp-strip__item">
|
|
<small>About us</small>
|
|
<h2>${escapeHtml(company.legalNameEn)}</h2>
|
|
<p>${escapeHtml(company.legalNameCn)}</p>
|
|
<p>${escapeHtml(company.address)}</p>
|
|
</article>
|
|
<article class="corp-strip__item">
|
|
<small>Official pages</small>
|
|
<p><a href="${escapeAttr(H5_PRIVACY_URL)}">Privacy Policy</a></p>
|
|
<p><a href="${escapeAttr(H5_TERMS_URL)}">Terms of Service</a></p>
|
|
<p><a href="${escapeAttr(H5_DELETE_ACCOUNT_URL)}">Delete Account</a></p>
|
|
<p><a href="${escapeAttr(H5_CHILD_SAFETY_URL)}">Child Safety Standards</a></p>
|
|
<p><a href="${escapeAttr(company.h5Website)}">H5 Landing Pages</a></p>
|
|
</article>
|
|
<article class="corp-strip__item" id="contact">
|
|
<small>Contact</small>
|
|
<p><a href="mailto:${escapeAttr(company.supportEmail)}">${escapeHtml(company.supportEmail)}</a></p>
|
|
<p><a href="tel:${escapeAttr(company.phone)}">${escapeHtml(company.phone)}</a></p>
|
|
<p>${escapeHtml(company.website)}</p>
|
|
</article>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
`,
|
|
});
|
|
}
|
|
|
|
function renderLegalPage(kind, options = {}) {
|
|
const { company, brand } = siteContent;
|
|
const doc = legalContent[kind];
|
|
const canonical = options.canonical || `${company.website}/${kind}/`;
|
|
const homeHref = options.homeHref || '/';
|
|
const privacyHref = options.privacyHref || H5_PRIVACY_URL;
|
|
const termsHref = options.termsHref || H5_TERMS_URL;
|
|
const primaryActionHref =
|
|
options.primaryActionHref || 'https://play.google.com/store/apps/details?id=com.org.yumi';
|
|
const primaryActionLabel = options.primaryActionLabel || 'Get Yumi';
|
|
return renderDocument({
|
|
title: `${doc.title} | ${brand.name}`,
|
|
description: doc.summary,
|
|
canonical,
|
|
hostRoot: '',
|
|
theme: kind === 'privacy' ? 'steel' : 'ink',
|
|
body: `
|
|
${renderHeader({
|
|
brandName: brand.name,
|
|
subline: doc.title,
|
|
links: [
|
|
{ href: homeHref, label: 'Home' },
|
|
{ href: privacyHref, label: 'Privacy' },
|
|
{ href: termsHref, label: 'Terms' },
|
|
],
|
|
action: { href: primaryActionHref, label: primaryActionLabel },
|
|
})}
|
|
<main class="shell legal-shell">
|
|
<div class="eyebrow">${escapeHtml(kind === 'privacy' ? 'Legal disclosure' : 'Service terms')}</div>
|
|
<h1 class="legal-title">${escapeHtml(doc.title)}</h1>
|
|
<p class="legal-summary">${escapeHtml(doc.summary)}</p>
|
|
<div class="legal-meta">
|
|
<div class="meta-chip">Effective date: ${escapeHtml(legalContent.effectiveDate)}</div>
|
|
<div class="meta-chip">${escapeHtml(company.legalNameEn)}</div>
|
|
<div class="meta-chip">${escapeHtml(company.privacyEmail)}</div>
|
|
</div>
|
|
<div class="legal-stack">
|
|
${doc.sections.map((section) => `
|
|
<section class="section-card legal-block">
|
|
<h3>${escapeHtml(section.title)}</h3>
|
|
${renderParagraphs(section.paragraphs)}
|
|
${renderBullets(section.bullets)}
|
|
</section>
|
|
`).join('')}
|
|
</div>
|
|
</main>
|
|
${renderFooter({ company, brandName: brand.name })}
|
|
`,
|
|
});
|
|
}
|
|
|
|
function renderPlainLegalPage(kind, options = {}) {
|
|
const { company, brand } = siteContent;
|
|
const doc = legalContent[kind];
|
|
const canonical = options.canonical || `${company.h5Website}/${kind}.html`;
|
|
return `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>${escapeHtml(doc.title)} | ${escapeHtml(brand.name)}</title>
|
|
<meta name="description" content="${escapeAttr(doc.summary)}">
|
|
<link rel="canonical" href="${escapeAttr(canonical)}">
|
|
</head>
|
|
<body>
|
|
<h1>${escapeHtml(doc.title)}</h1>
|
|
<p>Effective date: ${escapeHtml(legalContent.effectiveDate)}</p>
|
|
<p>Operator: ${escapeHtml(company.legalNameEn)} (${escapeHtml(company.legalNameCn)})</p>
|
|
<p>Address: ${escapeHtml(company.address)}</p>
|
|
<p>Contact: ${escapeHtml(company.privacyEmail)} / ${escapeHtml(company.phone)}</p>
|
|
${kind === 'deleteAccount' ? `<p><a href="${escapeAttr(renderDeleteAccountMailto(company.privacyEmail))}">Start a deletion request by email</a></p>` : ''}
|
|
<p>${escapeHtml(doc.summary)}</p>
|
|
${doc.sections.map((section) => renderPlainLegalSection(section)).join('\n')}
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
function renderLandingIndexPage() {
|
|
const { brand, company } = siteContent;
|
|
return renderDocument({
|
|
title: `${brand.name} H5 | Campaign Pages`,
|
|
description: 'Index of JSON-driven Yumi landing pages.',
|
|
canonical: `${company.h5Website}/`,
|
|
hostRoot: '',
|
|
theme: 'sapphire',
|
|
body: `
|
|
${renderHeader({
|
|
brandName: brand.name,
|
|
subline: 'H5 landing pages',
|
|
links: [
|
|
{ href: '/', label: 'H5 Home' },
|
|
{ href: '/studio/', label: 'Studio' },
|
|
{ href: 'https://www.haiyihy.com/', label: 'Official Site' },
|
|
],
|
|
action: { href: H5_PRIVACY_URL, label: 'Privacy' },
|
|
})}
|
|
<main class="shell landing-shell">
|
|
<div class="eyebrow">Operations and campaigns</div>
|
|
<h1 class="landing-title">A structured index of all public H5 destinations.</h1>
|
|
<p class="landing-lead">Each page below is generated from a single JSON file, which keeps campaign updates lightweight and predictable.</p>
|
|
<div class="landing-grid" style="margin-top: 28px;">
|
|
${landingPages.map((page) => `
|
|
<a class="link-card" href="/${escapeAttr(page.slug)}/">
|
|
<small>${escapeHtml(page.eyebrow)}</small>
|
|
<h3>${escapeHtml(page.title)}</h3>
|
|
<p>${escapeHtml(page.summary)}</p>
|
|
<span>Open page</span>
|
|
</a>
|
|
`).join('')}
|
|
</div>
|
|
</main>
|
|
${renderFooter({ company, brandName: brand.name })}
|
|
`,
|
|
});
|
|
}
|
|
|
|
function renderLandingPage(page) {
|
|
const { brand, company } = siteContent;
|
|
return renderDocument({
|
|
title: `${page.title} | ${brand.name} H5`,
|
|
description: page.summary,
|
|
canonical: `${company.h5Website}/${page.slug}/`,
|
|
hostRoot: '../',
|
|
theme: page.theme || 'ember',
|
|
body: `
|
|
${renderHeader({
|
|
brandName: brand.name,
|
|
subline: 'H5 landing page',
|
|
links: [
|
|
{ href: '/', label: 'H5 Home' },
|
|
{ href: '/studio/', label: 'Studio' },
|
|
{ href: 'https://www.haiyihy.com/', label: 'Official Site' },
|
|
],
|
|
action: { href: H5_PRIVACY_URL, label: 'Privacy' },
|
|
})}
|
|
<main class="shell landing-shell">
|
|
<div class="eyebrow">${escapeHtml(page.eyebrow)}</div>
|
|
<h1 class="landing-title">${escapeHtml(page.title)}</h1>
|
|
<p class="landing-lead">${escapeHtml(page.summary)}</p>
|
|
<div class="landing-meta">
|
|
${page.stats.map((item) => `<div class="meta-chip"><strong>${escapeHtml(item.value)}</strong> · ${escapeHtml(item.label)}</div>`).join('')}
|
|
</div>
|
|
<div class="landing-banner">
|
|
<p>${escapeHtml(page.highlight)}</p>
|
|
</div>
|
|
<div class="actions">
|
|
<a class="button" href="${escapeAttr(page.cta.href)}">${escapeHtml(page.cta.label)}</a>
|
|
<a class="button-secondary" href="${escapeAttr(page.secondaryCta.href)}">${escapeHtml(page.secondaryCta.label)}</a>
|
|
</div>
|
|
<div class="landing-stack">
|
|
${page.sections.map((section) => `
|
|
<section class="section-card">
|
|
<h3>${escapeHtml(section.title)}</h3>
|
|
<p>${escapeHtml(section.body)}</p>
|
|
${renderBullets(section.bullets)}
|
|
</section>
|
|
`).join('')}
|
|
</div>
|
|
</main>
|
|
${renderFooter({ company, brandName: brand.name })}
|
|
`,
|
|
});
|
|
}
|
|
|
|
function renderStudioPage() {
|
|
const { brand, company } = siteContent;
|
|
return renderDocument({
|
|
title: `${brand.name} H5 Studio`,
|
|
description: 'Editorial cockpit for landing page content files.',
|
|
canonical: `${company.h5Website}/studio/`,
|
|
hostRoot: '../',
|
|
theme: 'forest',
|
|
body: `
|
|
${renderHeader({
|
|
brandName: brand.name,
|
|
subline: 'Content studio',
|
|
links: [
|
|
{ href: '/', label: 'H5 Home' },
|
|
{ href: 'https://www.haiyihy.com/', label: 'Official Site' },
|
|
{ href: H5_PRIVACY_URL, label: 'Privacy' },
|
|
],
|
|
action: { href: '/', label: 'View Pages' },
|
|
})}
|
|
<main class="shell studio-shell">
|
|
<div class="eyebrow">Editorial cockpit</div>
|
|
<h1 class="landing-title">Manage landing pages from JSON, not from page code.</h1>
|
|
<p class="landing-lead">This page is a publishing reference for operators and developers. It documents the current slugs and shows the exact JSON structure that drives each landing page.</p>
|
|
<div class="studio-note">
|
|
Current management mode is file-driven. Edit JSON under <code>public-site/content/landing-pages</code>, rebuild the site, and redeploy. This keeps operations fast while avoiding app-release coupling.
|
|
</div>
|
|
<div class="landing-stack">
|
|
${landingPages.map((page) => `
|
|
<section class="section-card">
|
|
<small>${escapeHtml(page.slug)}</small>
|
|
<h3>${escapeHtml(page.title)}</h3>
|
|
<p>${escapeHtml(page.summary)}</p>
|
|
<div class="actions">
|
|
<a class="button-secondary" href="/${escapeAttr(page.slug)}/">Preview page</a>
|
|
</div>
|
|
<pre class="code-block">${escapeHtml(JSON.stringify(page, null, 2))}</pre>
|
|
</section>
|
|
`).join('')}
|
|
</div>
|
|
</main>
|
|
${renderFooter({ company, brandName: brand.name })}
|
|
`,
|
|
});
|
|
}
|
|
|
|
function renderHeader({ brandName, subline, links, action }) {
|
|
return `
|
|
<header class="site-header">
|
|
<div class="shell site-header__inner">
|
|
<a class="brand" href="/">
|
|
<span class="brand__mark" aria-hidden="true"></span>
|
|
<span class="brand__text">
|
|
<span class="brand__name">${escapeHtml(brandName)}</span>
|
|
<span class="brand__sub">${escapeHtml(subline)}</span>
|
|
</span>
|
|
</a>
|
|
<nav class="header-links" aria-label="Primary">
|
|
${links.map((item) => `<a href="${escapeAttr(item.href)}">${escapeHtml(item.label)}</a>`).join('')}
|
|
<a class="header-pill" href="${escapeAttr(action.href)}">${escapeHtml(action.label)}</a>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
`;
|
|
}
|
|
|
|
function renderFooter({ company, brandName }) {
|
|
return `
|
|
<footer class="site-footer">
|
|
<div class="shell">
|
|
<div class="footer-grid">
|
|
<div>
|
|
<div class="eyebrow">Legal and support</div>
|
|
<p>${escapeHtml(brandName)} is operated by ${escapeHtml(company.legalNameEn)} (${escapeHtml(company.legalNameCn)}).</p>
|
|
<p>${escapeHtml(company.address)}</p>
|
|
<p>Industry: ${escapeHtml(company.industry)}</p>
|
|
<p>Support: <a href="mailto:${escapeAttr(company.supportEmail)}">${escapeHtml(company.supportEmail)}</a><br>Phone: <a href="tel:${escapeAttr(company.phone)}">${escapeHtml(company.phone)}</a></p>
|
|
</div>
|
|
<div>
|
|
<div class="footer-links">
|
|
<a href="https://www.haiyihy.com/">Official site</a>
|
|
<a href="${escapeAttr(H5_PRIVACY_URL)}">Privacy policy</a>
|
|
<a href="${escapeAttr(H5_TERMS_URL)}">Terms of service</a>
|
|
<a href="${escapeAttr(H5_DELETE_ACCOUNT_URL)}">Delete account</a>
|
|
<a href="${escapeAttr(H5_CHILD_SAFETY_URL)}">Child safety</a>
|
|
<a href="https://h5.haiyihy.com/">H5 index</a>
|
|
<a href="https://h5.haiyihy.com/studio/">H5 studio</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
`;
|
|
}
|
|
|
|
function renderDocument({ title, description, canonical, theme, hostRoot, body, bodyClass = '' }) {
|
|
return `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>${escapeHtml(title)}</title>
|
|
<meta name="description" content="${escapeAttr(description)}">
|
|
<link rel="canonical" href="${escapeAttr(canonical)}">
|
|
<link rel="stylesheet" href="${escapeAttr(hostRoot)}assets/site.css">
|
|
</head>
|
|
<body data-theme="${escapeAttr(theme)}"${bodyClass ? ` class="${escapeAttr(bodyClass)}"` : ''}>
|
|
${body}
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
function renderParagraphs(paragraphs = []) {
|
|
return paragraphs.map((paragraph) => `<p>${escapeHtml(paragraph)}</p>`).join('');
|
|
}
|
|
|
|
function renderBullets(bullets = []) {
|
|
if (!bullets?.length) {
|
|
return '';
|
|
}
|
|
return `<ul>${bullets.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`;
|
|
}
|
|
|
|
function renderPlainLegalSection(section) {
|
|
return `
|
|
<h2>${escapeHtml(section.title)}</h2>
|
|
${renderParagraphs(section.paragraphs)}
|
|
${renderPlainBullets(section.bullets)}
|
|
`;
|
|
}
|
|
|
|
function renderPlainBullets(bullets = []) {
|
|
if (!bullets?.length) {
|
|
return '';
|
|
}
|
|
return bullets.map((item) => `<p>- ${escapeHtml(item)}</p>`).join('');
|
|
}
|
|
|
|
function renderDeleteAccountMailto(email) {
|
|
const subject = encodeURIComponent('Delete Yumi Account');
|
|
return `mailto:${email}?subject=${subject}`;
|
|
}
|
|
|
|
function renderSiteMap() {
|
|
const pages = [
|
|
'https://www.haiyihy.com/',
|
|
'https://h5.haiyihy.com/',
|
|
H5_PRIVACY_URL,
|
|
H5_TERMS_URL,
|
|
H5_DELETE_ACCOUNT_URL,
|
|
H5_CHILD_SAFETY_URL,
|
|
'https://h5.haiyihy.com/studio/',
|
|
...landingPages.map((page) => `https://h5.haiyihy.com/${page.slug}/`),
|
|
];
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
${pages.map((url) => ` <url><loc>${url}</loc></url>`).join('\n')}
|
|
</urlset>
|
|
`;
|
|
}
|
|
|
|
function renderRedirectPage({ title, target }) {
|
|
return `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta http-equiv="refresh" content="0;url=${escapeAttr(target)}">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>${escapeHtml(title)}</title>
|
|
<link rel="canonical" href="${escapeAttr(target)}">
|
|
<script>window.location.replace(${JSON.stringify(target)});</script>
|
|
</head>
|
|
<body>
|
|
<p>Redirecting to <a href="${escapeAttr(target)}">${escapeHtml(target)}</a>...</p>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
function readJson(filePath) {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
}
|
|
|
|
function ensureDir(dirPath) {
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
}
|
|
|
|
function writeHtml(filePath, content) {
|
|
ensureDir(path.dirname(filePath));
|
|
fs.writeFileSync(filePath, `${content}\n`, 'utf8');
|
|
}
|
|
|
|
function writeText(filePath, content) {
|
|
ensureDir(path.dirname(filePath));
|
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
}
|
|
|
|
function copyFile(source, target) {
|
|
ensureDir(path.dirname(target));
|
|
fs.copyFileSync(source, target);
|
|
}
|
|
|
|
function escapeHtml(value = '') {
|
|
return String(value)
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
function escapeAttr(value = '') {
|
|
return escapeHtml(value);
|
|
}
|