Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Marked } from 'marked';
import { docShell, esc, GITHUB_URL, highlightTokens, SITE_URL } from './src/layout.mjs';
import { renderLanding } from './src/landing.mjs';
import { renderImpressum } from './src/impressum.mjs';
import { renderLicense } from './src/license.mjs';

const root = path.dirname(fileURLToPath(import.meta.url));
const dist = path.join(root, 'dist');
Expand All @@ -32,6 +33,15 @@ function locateDocs() {

const docsDir = locateDocs();

// --- license: raw text from the tool repo root, next to docs/ ---------------

const licensePath = path.join(docsDir, '..', 'LICENSE');
if (!existsSync(licensePath)) {
console.error(`error: LICENSE not found at ${licensePath}`);
process.exit(1);
}
const licenseText = readFileSync(licensePath, 'utf8');

// --- version: release tag from env, else the tool repo's package.json ------

function readVersion() {
Expand Down Expand Up @@ -174,13 +184,21 @@ writeFileSync(path.join(dist, 'index.html'), renderLanding({ version, gitRef }))
mkdirSync(path.join(dist, 'impressum'), { recursive: true });
writeFileSync(path.join(dist, 'impressum', 'index.html'), renderImpressum({ version }));

mkdirSync(path.join(dist, 'license'), { recursive: true });
writeFileSync(path.join(dist, 'license', 'index.html'), renderLicense({ version, text: licenseText }));

for (const page of pages) {
const dir = page.slug ? path.join(dist, 'docs', page.slug) : path.join(dist, 'docs');
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, 'index.html'), renderDoc(page));
}

const sitePaths = ['/', ...pages.map((p) => (p.slug ? `/docs/${p.slug}/` : '/docs/')), '/impressum/'];
const sitePaths = [
'/',
...pages.map((p) => (p.slug ? `/docs/${p.slug}/` : '/docs/')),
'/license/',
'/impressum/',
];
writeFileSync(
path.join(dist, 'sitemap.xml'),
`<?xml version="1.0" encoding="UTF-8"?>
Expand All @@ -194,4 +212,4 @@ cpSync(path.join(root, 'public'), dist, { recursive: true });
cpSync(path.join(root, 'src', 'styles', 'site.css'), path.join(dist, 'site.css'));
cpSync(path.join(root, 'src', 'scripts', 'site.js'), path.join(dist, 'site.js'));

console.log(`built ${pages.length + 2} pages into dist/ (flashtrace ${version || 'unknown version'})`);
console.log(`built ${pages.length + 3} pages into dist/ (flashtrace ${version || 'unknown version'})`);
2 changes: 1 addition & 1 deletion src/layout.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function footer({ version }) {
<a href="/docs/">Docs</a>
<a href="${GITHUB_URL}">GitHub</a>
<a href="${DISCUSSIONS_URL}">Discussions</a>
<a href="${GITHUB_URL}/blob/main/LICENSE">License</a>
<a href="/license/">License</a>
<a href="/impressum/">Legal Notice</a>
</nav>
<p class="footer-note">Docs built from flashtrace ${esc(version)}.</p>
Expand Down
66 changes: 66 additions & 0 deletions src/license.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// License page - renders the tool repo's LICENSE (plain text, pre-formatted
// with spaces for centering) verbatim inside the site shell. When the text is
// recognized as Apache 2.0, a GitHub-style summary panel is shown above it;
// an unrecognized license falls back to the raw text alone rather than
// risking a wrong summary.
import { esc, pageShell } from './layout.mjs';

// Octicons (MIT): law, check, x, info.
const lawIcon = `<svg viewBox="0 0 24 24" width="28" height="28" fill="currentColor" aria-hidden="true"><path d="M12.75 2.75V4.5h1.975c.351 0 .694.106.984.303l1.697 1.154c.041.028.09.043.14.043h4.102a.75.75 0 0 1 0 1.5H20.07l3.366 7.68a.749.749 0 0 1-.23.896c-.1.074-.203.143-.31.206a6.296 6.296 0 0 1-.79.399 7.349 7.349 0 0 1-2.856.569 7.343 7.343 0 0 1-2.855-.568 6.205 6.205 0 0 1-.79-.4 3.205 3.205 0 0 1-.307-.202l-.005-.004a.749.749 0 0 1-.23-.896l3.368-7.68h-.886c-.351 0-.694-.106-.984-.303l-1.697-1.154a.246.246 0 0 0-.14-.043H12.75v14.5h4.487a.75.75 0 0 1 0 1.5H6.763a.75.75 0 0 1 0-1.5h4.487V6H9.275a.249.249 0 0 0-.14.043L7.439 7.197c-.29.197-.633.303-.984.303h-.886l3.368 7.68a.75.75 0 0 1-.209.878c-.08.065-.16.126-.31.223a6.077 6.077 0 0 1-.792.433 6.924 6.924 0 0 1-2.876.62 6.913 6.913 0 0 1-2.876-.62 6.077 6.077 0 0 1-.792-.433 3.483 3.483 0 0 1-.309-.221.762.762 0 0 1-.21-.88L3.93 7.5H2.353a.75.75 0 0 1 0-1.5h4.102c.05 0 .099-.015.141-.043l1.695-1.154c.29-.198.634-.303.985-.303h1.974V2.75a.75.75 0 0 1 1.5 0ZM2.193 15.198a5.414 5.414 0 0 0 2.557.635 5.414 5.414 0 0 0 2.557-.635L4.75 9.368Zm14.51-.024c.082.04.174.083.275.126.53.223 1.305.45 2.272.45a5.847 5.847 0 0 0 2.547-.576L19.25 9.367Z"/></svg>`;
const checkIcon = `<svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor" aria-hidden="true"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/></svg>`;
const xIcon = `<svg viewBox="0 0 12 12" width="13" height="13" fill="currentColor" aria-hidden="true"><path d="M2.22 2.22a.749.749 0 0 1 1.06 0L6 4.939 8.72 2.22a.749.749 0 1 1 1.06 1.06L7.061 6 9.78 8.72a.749.749 0 1 1-1.06 1.06L6 7.061 3.28 9.78a.749.749 0 1 1-1.06-1.06L4.939 6 2.22 3.28a.749.749 0 0 1 0-1.06Z"/></svg>`;
const infoIcon = `<svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>`;

// Summary as shown by GitHub's license banner (sourced from choosealicense.com).
function apacheSummaryPanel() {
const col = (label, cls, icon, items) => `<div class="license-col">
<p class="license-col-label">${label}</p>
<ul>${items.map((it) => `<li class="${cls}">${icon}<span>${esc(it)}</span></li>`).join('\n')}</ul>
</div>`;
return `<section class="license-summary" aria-label="License summary">
<div class="license-summary-head">
${lawIcon}
<div>
<p class="license-summary-kicker">flashtrace is licensed under the</p>
<p class="license-summary-name">Apache License 2.0</p>
</div>
</div>
<p class="license-summary-desc">A permissive license whose main conditions require preservation of copyright and license notices. Contributors provide an express grant of patent rights. Licensed works, modifications, and larger works may be distributed under different terms and without source code.</p>
<div class="license-summary-cols">
${col('Permissions', 'lic-ok', checkIcon, ['Commercial use', 'Modification', 'Distribution', 'Patent use', 'Private use'])}
${col('Limitations', 'lic-no', xIcon, ['Trademark use', 'Liability', 'Warranty'])}
${col('Conditions', 'lic-info', infoIcon, ['License and copyright notice', 'State changes'])}
</div>
<p class="license-summary-note">This is not legal advice. <a href="https://choosealicense.com/licenses/apache-2.0/" rel="external">Learn more about the Apache License 2.0</a>.</p>
</section>`;
}

// Apache 2.0 starts with "Apache License" / "Version 2.0, January 2004" as
// its first two non-blank lines. Checking only the header (not the whole
// text) avoids a false match on a future license that merely mentions it.
function isApache2(text) {
const [l1 = '', l2 = ''] = text
.split(/\r?\n/, 20)
.map((l) => l.trim())
.filter(Boolean);
return l1 === 'Apache License' && l2.startsWith('Version 2.0');
}

export function renderLicense({ version, text }) {
const body = `<main class="section" id="main">
<div class="doc-content legal license-doc">
<h1>License</h1>
${isApache2(text) ? `${apacheSummaryPanel()}\n<p class="license-divider" aria-hidden="true">Full license text</p>` : ''}
<pre>${esc(text)}</pre>
</div>
</main>`;
return pageShell({
title: 'License 路 flashtrace',
description: 'The Apache License, Version 2.0 that flashtrace is distributed under.',
path: '/license/',
version,
active: '',
body,
bodyClass: 'page-legal',
});
}
147 changes: 147 additions & 0 deletions src/styles/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
--tk-id: #b45309;
--tk-kw: #92400e;
--tk-arrow: #0e7490;
--ok: #15803d;
--no: #b91c1c;
--info: #0e7490;
--shadow: 0 1px 3px rgb(0 0 0 / 0.08), 0 8px 24px rgb(0 0 0 / 0.06);
--topbar-h: 3.5rem;
color-scheme: light;
Expand All @@ -39,6 +42,9 @@
--tk-id: #f9c21d;
--tk-kw: #e8964a;
--tk-arrow: #56b6c2;
--ok: #3fb950;
--no: #f85149;
--info: #56b6c2;
--shadow: 0 1px 3px rgb(0 0 0 / 0.5), 0 8px 24px rgb(0 0 0 / 0.35);
color-scheme: dark;
}
Expand All @@ -61,6 +67,9 @@
--tk-id: #f9c21d;
--tk-kw: #e8964a;
--tk-arrow: #56b6c2;
--ok: #3fb950;
--no: #f85149;
--info: #56b6c2;
color-scheme: dark;
}
}
Expand Down Expand Up @@ -993,12 +1002,150 @@ h3:hover .heading-anchor,
padding-bottom: 3.5rem;
}

.page-legal .doc-content {
margin-inline: auto;
}

.legal address {
font-style: normal;
line-height: 1.7;
color: var(--text);
}

/* license: the tool repo's raw LICENSE text. Shrink-wrap the block and center
it so the text's own space-centered header reads as intended. */
.license-doc {
max-width: 52rem;
}

.license-doc h1 {
text-align: center;
}

.license-doc pre {
width: fit-content;
max-width: 100%;
margin: 1.75rem auto 0;
padding: 2rem 2.5rem;
font-size: 0.9rem;
}

/* GitHub-style summary panel above the raw license text */
.license-summary {
margin-top: 1.75rem;
padding: 1.5rem 1.75rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg-raised);
box-shadow: var(--shadow);
}

.license-summary-head {
display: flex;
align-items: center;
gap: 1rem;
color: var(--muted);
}

.license-summary-kicker {
margin: 0;
font-size: 0.85rem;
color: var(--muted);
}

.license-summary-name {
margin: 0;
font-size: 1.15rem;
font-weight: 650;
line-height: 1.3;
color: var(--text);
}

.license-summary-desc {
margin: 1rem 0 0;
font-size: 0.9rem;
color: var(--muted);
}

.license-summary-cols {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem 1.5rem;
margin-top: 1.5rem;
}

.license-col-label {
margin: 0 0 0.4rem;
font-size: 0.75rem;
font-weight: 650;
text-transform: uppercase;
letter-spacing: 0.05em;
}

.license-col ul {
list-style: none;
margin: 0;
padding: 0;
}

.license-col li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.15rem 0;
font-size: 0.85rem;
}

.license-col li svg {
flex-shrink: 0;
}

.lic-ok svg {
color: var(--ok);
}

.lic-no svg {
color: var(--no);
}

.lic-info svg {
color: var(--info);
}

.license-summary-note {
margin: 1.5rem 0 0;
padding-top: 1rem;
border-top: 1px solid var(--border);
font-size: 0.8rem;
color: var(--muted);
}

/* labeled divider between the summary panel and the raw text */
.license-divider {
display: flex;
align-items: center;
gap: 1rem;
margin: 3rem 0 0;
color: var(--muted);
font-size: 0.75rem;
font-weight: 650;
text-transform: uppercase;
letter-spacing: 0.08em;
}

.license-divider::before,
.license-divider::after {
content: '';
flex: 1;
border-top: 1px solid var(--border);
}

@media (max-width: 640px) {
.license-summary-cols {
grid-template-columns: 1fr;
}
}

/* ---------- responsive ---------- */

@media (max-width: 1100px) {
Expand Down