A CLI for publishing standard.site documents to ATProto
at main 199 lines 4.9 kB view raw
1import { existsSync, readFileSync } from "fs"; 2 3interface ThemeVars { 4 fgColor: string; 5 bgColor: string; 6 accentColor: string; 7 borderColor: string; 8 errorColor: string; 9 borderRadius: string; 10 fontFamily: string; 11 darkBgColor: string; 12 darkFgColor: string; 13 darkBorderColor: string; 14 darkErrorColor: string; 15} 16 17function getThemeVars(): ThemeVars { 18 return { 19 fgColor: process.env.THEME_FG_COLOR || "#2C2C2C", 20 bgColor: process.env.THEME_BG_COLOR || "#F5F3EF", 21 accentColor: process.env.THEME_ACCENT_COLOR || "#3A5A40", 22 borderColor: process.env.THEME_BORDER_COLOR || "#D5D1C8", 23 errorColor: process.env.THEME_ERROR_COLOR || "#8B3A3A", 24 borderRadius: process.env.THEME_BORDER_RADIUS || "6px", 25 fontFamily: process.env.THEME_FONT_FAMILY || "system-ui, sans-serif", 26 darkBgColor: process.env.THEME_DARK_BG_COLOR || "#1A1A1A", 27 darkFgColor: process.env.THEME_DARK_FG_COLOR || "#E5E5E5", 28 darkBorderColor: process.env.THEME_DARK_BORDER_COLOR || "#3A3A3A", 29 darkErrorColor: process.env.THEME_DARK_ERROR_COLOR || "#E57373", 30 }; 31} 32 33function getCustomCss(): string { 34 const cssPath = process.env.THEME_CSS_PATH; 35 if (!cssPath) return ""; 36 try { 37 if (existsSync(cssPath)) { 38 return readFileSync(cssPath, "utf-8"); 39 } 40 } catch { 41 console.warn(`Failed to read custom CSS file: ${cssPath}`); 42 } 43 return ""; 44} 45 46export function generateStyleBlock(): string { 47 const t = getThemeVars(); 48 const customCss = getCustomCss(); 49 50 return `<style> 51 :root { 52 --sequoia-fg-color: ${t.fgColor}; 53 --sequoia-bg-color: ${t.bgColor}; 54 --sequoia-accent-color: ${t.accentColor}; 55 --sequoia-border-color: ${t.borderColor}; 56 --sequoia-error-color: ${t.errorColor}; 57 --sequoia-border-radius: ${t.borderRadius}; 58 --sequoia-font-family: ${t.fontFamily}; 59 } 60 61 @media (prefers-color-scheme: dark) { 62 :root { 63 --sequoia-fg-color: ${t.darkFgColor}; 64 --sequoia-bg-color: ${t.darkBgColor}; 65 --sequoia-border-color: ${t.darkBorderColor}; 66 --sequoia-error-color: ${t.darkErrorColor}; 67 } 68 } 69 70 * { box-sizing: border-box; margin: 0; padding: 0; } 71 72 body { 73 font-family: var(--sequoia-font-family); 74 background: var(--sequoia-bg-color); 75 color: var(--sequoia-fg-color); 76 line-height: 1.6; 77 } 78 79 .page-container { 80 max-width: 480px; 81 margin: 4rem auto; 82 padding: 0 1.25rem; 83 } 84 85 h1 { 86 font-size: 1.75rem; 87 font-weight: 700; 88 margin-bottom: 0.75rem; 89 } 90 91 p { margin-bottom: 1rem; } 92 93 a { 94 color: var(--sequoia-accent-color); 95 text-decoration: underline; 96 } 97 98 a:hover { text-decoration: none; } 99 100 form { display: flex; flex-direction: column; } 101 102 input[type="text"] { 103 padding: 0.5rem 0.75rem; 104 border: 1px solid var(--sequoia-border-color); 105 border-radius: var(--sequoia-border-radius); 106 margin-bottom: 1.25rem; 107 width: 100%; 108 font-size: 1rem; 109 font-family: inherit; 110 background: var(--sequoia-bg-color); 111 color: var(--sequoia-fg-color); 112 } 113 114 input[type="text"]:focus { 115 border-color: var(--sequoia-accent-color); 116 outline: 2px solid var(--sequoia-accent-color); 117 outline-offset: 2px; 118 } 119 120 button { 121 padding: 0.625rem 1.25rem; 122 background: var(--sequoia-accent-color); 123 color: #fff; 124 border: none; 125 border-radius: var(--sequoia-border-radius); 126 font-size: 1rem; 127 font-family: inherit; 128 font-weight: 600; 129 cursor: pointer; 130 transition: opacity 0.15s; 131 } 132 133 button:hover { opacity: 0.9; } 134 135 button:focus-visible { 136 outline: 2px solid var(--sequoia-accent-color); 137 outline-offset: 2px; 138 } 139 140 table { 141 width: 100%; 142 border-collapse: collapse; 143 table-layout: fixed; 144 margin-top: 1rem; 145 } 146 147 td { 148 padding: 0.5rem 0.75rem; 149 border-bottom: 1px solid var(--sequoia-border-color); 150 vertical-align: top; 151 } 152 153 td:first-child { 154 width: 7rem; 155 font-weight: 600; 156 } 157 158 td:last-child { overflow: hidden; } 159 160 td code { 161 font-size: 0.85rem; 162 word-break: break-all; 163 } 164 165 td div { 166 overflow-x: auto; 167 white-space: nowrap; 168 } 169 170 .error { color: var(--sequoia-error-color); } 171 ${customCss ? `\n /* Custom CSS */\n ${customCss}` : ""} 172 </style>`; 173} 174 175export function page(body: string, headExtra = ""): string { 176 return `<!DOCTYPE html> 177<html lang="en"> 178<head> 179 <meta charset="UTF-8" /> 180 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 181 <title>Sequoia · Subscribe</title> 182 ${generateStyleBlock()} 183 ${headExtra} 184</head> 185<body> 186 <div class="page-container"> 187 ${body} 188 </div> 189</body> 190</html>`; 191} 192 193export function escapeHtml(text: string): string { 194 return text 195 .replace(/&/g, "&amp;") 196 .replace(/</g, "&lt;") 197 .replace(/>/g, "&gt;") 198 .replace(/"/g, "&quot;"); 199}