Files
opsxshell/tools/homepage-build/main.go
T
Logan.Li e3962a6334
CI / Lint & TypeCheck (push) Canceled after 0s
CI / Test (push) Canceled after 0s
CI / Tauri Foundation Acceptance (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Build (windows-latest) (push) Canceled after 0s
feat(homepage): 新增独立授权与激活页 /license + 补齐定价页样式基础类
- 新增 homepage/license.html → dist/license.html + dist/license/index.html:
  交付与激活四步(邮件交付授权码)、LIC- 在线 与 LICOFF- 离线 对比、
  授权服务器 licence.rlwj.cn 通信范围说明、授权类型与价格、10 条授权 FAQ
- 补 style.css 缺失的定价/授权页共享基础类(wrap/section/sec-title/price-card/
  plan-tag/deal-note 等,暗色主题版)—— 修复 /pricing 页此前内容贴左、
  价格卡无卡片样式的线上缺陷(仅新增规则,未改动既有样式)
- tools/homepage-build 支持可选单页,产出 <name>.html 与 <name>/index.html
- 导航/页脚接入 /license,子页跨页锚点补 / 前缀、品牌 logo 回首页
2026-10-16 04:19:20 +08:00

189 lines
6.1 KiB
Go

// homepage-build:把 homepage/ 源(落地页+文档 md)构建为纯静态站点 homepage/dist/。
// 产物可直接部署到任意静态托管(nginx/Pages)——官网与桌面二进制彻底解耦。
// 用法(独立 Go 模块,须在其目录内运行): cd tools/homepage-build && go run . -src ../../homepage -out ../../homepage/dist
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
gmhtml "github.com/yuin/goldmark/renderer/html"
"html/template"
)
type PageDef struct {
File string `json:"file"`
Slug string `json:"slug"`
Title string `json:"title"`
Updated string `json:"updated,omitempty"`
}
type GroupDef struct {
Name string `json:"name"`
Pages []PageDef `json:"pages"`
}
type Manifest struct {
ProductVersion string `json:"productVersion"`
Groups []GroupDef `json:"groups"`
}
type TocItem struct {
ID string
Level int
Text string
}
type SearchEntry struct {
Slug, Title, Group, Text string
}
type NavLink struct {
Slug, Title string
}
type Page struct {
Slug, Title, Group, Updated string
HTML string
TOC []TocItem
Prev, Next *NavLink
}
var (
hRe = regexp.MustCompile(`(?s)<(h[23])>(.*?)</h[23]>`)
tagRe = regexp.MustCompile(`<[^>]+>`)
mdNoise = regexp.MustCompile("(?s)```.*?```|`[^`]*`|!\\[[^\\]]*\\]\\([^)]*\\)|\\[[^\\]]*\\]\\([^)]*\\)|[#>*_\\-|]+")
)
func injectHeadingIDs(html string) (string, []TocItem) {
var toc []TocItem
counter := 0
out := hRe.ReplaceAllStringFunc(html, func(m string) string {
sub := hRe.FindStringSubmatch(m)
tag, inner := sub[1], sub[2]
counter++
id := fmt.Sprintf("sec-%d", counter)
text := strings.TrimSpace(tagRe.ReplaceAllString(inner, ""))
level := 2
if tag == "h3" {
level = 3
}
toc = append(toc, TocItem{ID: id, Level: level, Text: text})
return fmt.Sprintf(`<%s id="%s">%s</%s>`, tag, id, inner, tag)
})
return out, toc
}
func plainText(src string) string {
s := mdNoise.ReplaceAllString(src, " ")
s = strings.Join(strings.Fields(s), " ")
if len(s) > 6000 {
s = s[:6000]
}
return s
}
func must(err error, what string) {
if err != nil {
fmt.Fprintf(os.Stderr, "[homepage-build] %s: %v\n", what, err)
os.Exit(1)
}
}
func writeFile(path, content string) {
must(os.MkdirAll(filepath.Dir(path), 0o755), "mkdir "+path)
must(os.WriteFile(path, []byte(content), 0o644), "write "+path)
fmt.Println("[OK]", path)
}
// copyTree 幂等地把 src 目录整棵拷到 dst:先清空 dst 再 CopyFS。
// os.CopyFS 在目标文件已存在时会报错,故重建 dist 前必须先清空对应子目录。
func copyTree(dst, src string) {
must(os.RemoveAll(dst), "清理 "+dst)
must(os.CopyFS(dst, os.DirFS(src)), "拷 "+dst)
fmt.Println("[OK]", dst)
}
func main() {
src := flag.String("src", "homepage", "源目录")
out := flag.String("out", "homepage/dist", "产物目录")
flag.Parse()
// 1) 落地页与附加单页: 纯 HTML 直接拷 + site assets
// index.html 必需;pricing/terms/license 为可选页,源存在即产出。
// 附加页同时产出 <name>.html 与 <name>/index.html:兼容 nginx 的干净
// URL(location = /license) 与纯静态服务器(python -m http.server)。
for _, page := range []string{"index.html", "pricing.html", "terms.html", "license.html"} {
raw, err := os.ReadFile(filepath.Join(*src, page))
if err != nil {
if page == "index.html" {
must(err, "读 index.html")
}
continue
}
writeFile(filepath.Join(*out, page), string(raw))
if page != "index.html" {
writeFile(filepath.Join(*out, strings.TrimSuffix(page, ".html"), "index.html"), string(raw))
}
}
siteAssets := filepath.Join(*src, "assets")
if st, _ := os.Stat(siteAssets); st != nil {
copyTree(filepath.Join(*out, "site", "assets"), siteAssets)
}
// 2) 文档: manifest → goldmark 渲染 → docs.html 模板
mraw, err := os.ReadFile(filepath.Join(*src, "content", "manifest.json"))
must(err, "读 manifest")
var manifest Manifest
must(json.Unmarshal(mraw, &manifest), "解析 manifest")
tplRaw, err := os.ReadFile(filepath.Join(*src, "docs.html"))
must(err, "读 docs.html")
tpl, err := template.New("doc").Parse(string(tplRaw))
must(err, "解析 docs 模板")
md := goldmark.New(goldmark.WithExtensions(extension.Table, extension.Strikethrough, extension.TaskList), goldmark.WithRendererOptions(gmhtml.WithHardWraps()))
var order []string
pages := map[string]*Page{}
var search []SearchEntry
var prev *NavLink
for _, g := range manifest.Groups {
for _, def := range g.Pages {
srcb, err := os.ReadFile(filepath.Join(*src, "content", def.File))
must(err, "读文档 "+def.File)
var buf strings.Builder
must(md.Convert(srcb, &buf), "渲染 "+def.File)
body, toc := injectHeadingIDs(buf.String())
p := &Page{Slug: def.Slug, Title: def.Title, Group: g.Name, Updated: def.Updated, HTML: body, TOC: toc, Prev: prev}
pages[def.Slug] = p
order = append(order, def.Slug)
prev = &NavLink{Slug: def.Slug, Title: def.Title}
search = append(search, SearchEntry{Slug: def.Slug, Title: def.Title, Group: g.Name, Text: plainText(string(srcb))})
}
}
for i, slug := range order {
if i+1 < len(order) {
n := pages[order[i+1]]
pages[slug].Next = &NavLink{Slug: n.Slug, Title: n.Title}
}
}
// /docs/ 首页 = 第一篇(静态站无重定向,直接渲染)
render := func(p *Page) string {
var sb strings.Builder
must(tpl.Execute(&sb, map[string]any{"Manifest": manifest, "Page": p, "TOC": p.TOC, "Content": template.HTML(p.HTML)}), "渲染模板 "+p.Slug)
return sb.String()
}
writeFile(filepath.Join(*out, "docs", "index.html"), render(pages[order[0]]))
for _, slug := range order {
writeFile(filepath.Join(*out, "docs", slug, "index.html"), render(pages[slug]))
}
sj, _ := json.Marshal(search)
writeFile(filepath.Join(*out, "docs", "search-data.json"), string(sj))
// docs assets(模板引用 /docs/assets/*)
da := filepath.Join(*src, "docs-assets")
if st, _ := os.Stat(da); st != nil {
copyTree(filepath.Join(*out, "docs", "assets"), da)
}
fmt.Printf("[DONE] %d 页文档 + 落地页 → %s(纯静态,可部署任意域名)\n", len(order), *out)
}