build-public.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. import path from 'node:path';
  2. import fs from 'node:fs';
  3. import fsp from 'node:fs/promises';
  4. import { task } from './trace';
  5. import { treeDir, TreeFileType } from './lib/tree-dir';
  6. import type { TreeType, TreeTypeArray } from './lib/tree-dir';
  7. import { OUTPUT_MOCK_DIR, OUTPUT_MODULES_DIR, PUBLIC_DIR, ROOT_DIR } from './constants/dir';
  8. import { writeFile } from './lib/misc';
  9. import { fastStringCompare } from 'foxts/fast-string-compare';
  10. import type { VoidOrVoidArray } from './lib/misc';
  11. import picocolors from 'picocolors';
  12. import { tagged as html } from 'foxts/tagged';
  13. import { compareAndWriteFile } from './lib/create-file';
  14. import { appendArrayInPlace } from 'foxts/append-array-in-place';
  15. const priorityOrder: Record<'default' | string & {}, number> = {
  16. LICENSE: 0,
  17. domainset: 10,
  18. non_ip: 20,
  19. ip: 30,
  20. List: 40,
  21. Surge: 50,
  22. Clash: 60,
  23. 'sing-box': 70,
  24. Surfboard: 80,
  25. LegacyClashPremium: 81,
  26. Modules: 90,
  27. Script: 100,
  28. Mock: 110,
  29. Assets: 120,
  30. Internal: 130,
  31. default: Number.MAX_VALUE
  32. };
  33. const closedRootFolders = [
  34. 'Mock',
  35. 'Internal'
  36. ];
  37. async function copyDirContents(srcDir: string, destDir: string, promises: Array<Promise<VoidOrVoidArray>> = []): Promise<Array<Promise<VoidOrVoidArray>>> {
  38. for await (const entry of await fsp.opendir(srcDir)) {
  39. const src = path.join(srcDir, entry.name);
  40. const dest = path.join(destDir, entry.name);
  41. if (entry.isDirectory()) {
  42. console.warn(picocolors.red('[build public] cant copy directory'), src);
  43. } else {
  44. promises.push(fsp.copyFile(src, dest, fs.constants.COPYFILE_FICLONE));
  45. }
  46. }
  47. return promises;
  48. }
  49. export const buildPublic = task(require.main === module, __filename)(async (span) => {
  50. await span.traceChildAsync('copy rest of the files', async () => {
  51. const p: Array<Promise<any>> = [];
  52. fs.mkdirSync(OUTPUT_MODULES_DIR, { recursive: true });
  53. p.push(copyDirContents(path.join(ROOT_DIR, 'Modules'), OUTPUT_MODULES_DIR, p));
  54. fs.mkdirSync(OUTPUT_MOCK_DIR, { recursive: true });
  55. p.push(copyDirContents(path.join(ROOT_DIR, 'Mock'), OUTPUT_MOCK_DIR, p));
  56. await Promise.all(p);
  57. });
  58. const html = await span
  59. .traceChild('generate index.html')
  60. .traceAsyncFn(() => treeDir(PUBLIC_DIR).then(generateHtml));
  61. return Promise.all([
  62. compareAndWriteFile(
  63. span,
  64. appendArrayInPlace(
  65. [
  66. '/*',
  67. ' cache-control: public, max-age=300, stale-while-revalidate=30, stale-if-error=60',
  68. 'https://:project.pages.dev/*',
  69. ' X-Robots-Tag: noindex'
  70. ],
  71. Object.keys(priorityOrder).map((name) => `/${name}/*\n content-type: text/plain; charset=utf-8\n X-Robots-Tag: noindex`)
  72. ),
  73. path.join(PUBLIC_DIR, '_headers')
  74. ),
  75. compareAndWriteFile(
  76. span,
  77. [
  78. '# <pre>',
  79. '#########################################',
  80. '# Sukka\'s Ruleset - 404 Not Found',
  81. '################## EOF ##################</pre>'
  82. ],
  83. path.join(PUBLIC_DIR, '404.html')
  84. ),
  85. compareAndWriteFile(
  86. span,
  87. [
  88. '# This is a Robot-managed repo containing only output',
  89. '# The source code is located at [Sukkaw/Surge](https://github.com/Sukkaw/Surge)',
  90. '# Please follow the development at the source code repo instead',
  91. '',
  92. '![GitHub repo size](https://img.shields.io/github/repo-size/sukkalab/ruleset.skk.moe?style=flat-square)'
  93. ],
  94. path.join(PUBLIC_DIR, 'README.md')
  95. ),
  96. writeFile(path.join(PUBLIC_DIR, '.nojekyll'), ''),
  97. writeFile(path.join(PUBLIC_DIR, 'index.html'), html)
  98. ]);
  99. });
  100. const prioritySorter = (a: TreeType, b: TreeType) => ((priorityOrder[a.name] || priorityOrder.default) - (priorityOrder[b.name] || priorityOrder.default)) || fastStringCompare(a.name, b.name);
  101. function treeHtml(tree: TreeTypeArray, level = 0, closedFolderList: string[] = []): string {
  102. let result = '';
  103. tree.sort(prioritySorter);
  104. for (let i = 0, len = tree.length; i < len; i++) {
  105. const entry = tree[i];
  106. const open = closedFolderList.includes(entry.name) ? '' : (level === 0 ? 'open' : '');
  107. if (entry.type === TreeFileType.DIRECTORY) {
  108. result += html`
  109. <li class="folder">
  110. <details ${open}>
  111. <summary>${entry.name}</summary>
  112. <ul>${treeHtml(entry.children, level + 1)}</ul>
  113. </details>
  114. </li>
  115. `;
  116. } else if (/* entry.type === 'file' && */ !entry.name.endsWith('.html') && !entry.name.startsWith('_')) {
  117. result += html`
  118. <li class="file"><a class="file-link" href="${entry.path}">${entry.name}</a></li>
  119. `;
  120. }
  121. }
  122. return result;
  123. }
  124. function generateHtml(tree: TreeTypeArray) {
  125. return html`
  126. <!DOCTYPE html>
  127. <html lang="en">
  128. <head>
  129. <meta charset="utf-8">
  130. <title>Surge Ruleset Server | Sukka (@SukkaW)</title>
  131. <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
  132. <link href="https://cdn.skk.moe/favicon.ico" rel="icon" type="image/ico">
  133. <link href="https://cdn.skk.moe/favicon/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180">
  134. <link href="https://cdn.skk.moe/favicon/android-chrome-192x192.png" rel="icon" type="image/png" sizes="192x192">
  135. <link href="https://cdn.skk.moe/favicon/favicon-32x32.png" rel="icon" type="image/png" sizes="32x32">
  136. <link href="https://cdn.skk.moe/favicon/favicon-16x16.png" rel="icon" type="image/png" sizes="16x16">
  137. <meta name="description" content="Sukka 自用的 Surge / Clash Premium 规则组">
  138. <meta property="og:title" content="Surge Ruleset | Sukka (@SukkaW)">
  139. <meta property="og:type" content="Website">
  140. <meta property="og:url" content="https://ruleset.skk.moe/">
  141. <meta property="og:image" content="https://cdn.skk.moe/favicon/android-chrome-192x192.png">
  142. <meta property="og:description" content="Sukka 自用的 Surge / Clash Premium 规则组">
  143. <meta name="twitter:card" content="summary">
  144. <link rel="canonical" href="https://ruleset.skk.moe/">
  145. <style>
  146. :root {
  147. --font-family: system-ui, -apple-system, "Segoe UI", "Roboto", "Ubuntu", "Cantarell", "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  148. --line-height: 1.5;
  149. --font-weight: 400;
  150. --font-size: 16px;
  151. --border-radius: 0.25rem;
  152. --border-width: 1px;
  153. --outline-width: 3px;
  154. --spacing: 1rem;
  155. --typography-spacing-vertical: 1.5rem;
  156. --block-spacing-vertical: calc(var(--spacing)*2);
  157. --block-spacing-horizontal: var(--spacing);
  158. --nav-element-spacing-vertical: 1rem;
  159. --nav-element-spacing-horizontal: 0.5rem;
  160. --nav-link-spacing-vertical: 0.5rem;
  161. --nav-link-spacing-horizontal: 0.5rem;
  162. --form-label-font-weight: var(--font-weight);
  163. --transition: 0.2s ease-in-out
  164. }
  165. @media (min-width:576px) {
  166. :root {
  167. --font-size: 17px
  168. }
  169. }
  170. @media (min-width:768px) {
  171. :root {
  172. --font-size: 18px
  173. }
  174. }
  175. @media (min-width:992px) {
  176. :root {
  177. --font-size: 19px
  178. }
  179. }
  180. @media (min-width:1200px) {
  181. :root {
  182. --font-size: 20px
  183. }
  184. }
  185. a {
  186. --text-decoration: none
  187. }
  188. a.contrast,
  189. a.secondary {
  190. --text-decoration: underline
  191. }
  192. small {
  193. --font-size: 0.875em
  194. }
  195. h1,
  196. h2,
  197. h3,
  198. h4,
  199. h5,
  200. h6 {
  201. --font-weight: 700
  202. }
  203. h1 {
  204. --font-size: 2rem;
  205. --typography-spacing-vertical: 3rem
  206. }
  207. :not(thead):not(tfoot)>*>td {
  208. --font-size: 0.875em
  209. }
  210. code,
  211. kbd,
  212. pre,
  213. samp {
  214. --font-family: "Menlo", "Consolas", "Roboto Mono", "Ubuntu Monospace", "Noto Mono", "Oxygen Mono", "Liberation Mono", monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
  215. }
  216. kbd {
  217. --font-weight: bolder
  218. }
  219. :root:not([data-theme=dark]),
  220. [data-theme=light] {
  221. --background-color: #fff;
  222. --color: #415462;
  223. --h1-color: #1b2832;
  224. --h2-color: #24333e;
  225. --h3-color: #2c3d49;
  226. --h4-color: #374956;
  227. --h5-color: #415462;
  228. --h6-color: #4d606d;
  229. --muted-color: #73828c;
  230. --muted-border-color: #edf0f3;
  231. --primary: #1095c1;
  232. --primary-hover: #08769b;
  233. --primary-focus: rgba(16, 149, 193, .125);
  234. --primary-inverse: #fff;
  235. --secondary: #596b78;
  236. --secondary-hover: #415462;
  237. --secondary-focus: rgba(89, 107, 120, .125);
  238. --secondary-inverse: #fff;
  239. --contrast: #1b2832;
  240. --contrast-hover: #000;
  241. --contrast-focus: rgba(89, 107, 120, .125);
  242. --contrast-inverse: #fff;
  243. --mark-background-color: #fff2ca;
  244. --mark-color: #543a26;
  245. --modal-overlay-background-color: rgba(213, 220, 226, .8);
  246. --loading-spinner-opacity: 0.5;
  247. --icon-folder: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23415462' stroke-width='1.5' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.06-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.38a1.5 1.5 0 0 1-1.06-.44Z'/%3E%3C/svg%3E");
  248. --icon-file: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23415462' stroke-width='1.5' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19.5 14.25v-2.63a3.38 3.38 0 0 0-3.38-3.37h-1.5a1.13 1.13 0 0 1-1.12-1.13v-1.5a3.38 3.38 0 0 0-3.38-3.37H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.62c-.62 0-1.12.5-1.12 1.13v17.25c0 .62.5 1.12 1.13 1.12h12.75c.62 0 1.12-.5 1.12-1.13v-9.37a9 9 0 0 0-9-9Z'/%3E%3C/svg%3E");
  249. --icon-folder-open: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23415462' stroke-width='1.5' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M3.75 9.78c.11-.02.23-.03.34-.03h15.82c.11 0 .23 0 .34.03m-16.5 0a2.25 2.25 0 0 0-1.88 2.54l.85 6a2.25 2.25 0 0 0 2.23 1.93h14.1a2.25 2.25 0 0 0 2.23-1.93l.85-6a2.25 2.25 0 0 0-1.88-2.54m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.88a1.5 1.5 0 0 1 1.06.44l2.12 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.78'/%3E%3C/svg%3E");
  250. color-scheme: light
  251. }
  252. @media only screen and (prefers-color-scheme:dark) {
  253. :root:not([data-theme=light]) {
  254. --background-color: #11191f;
  255. --color: #bbc6ce;
  256. --h1-color: #edf0f3;
  257. --h2-color: #e1e6eb;
  258. --h3-color: #d5dce2;
  259. --h4-color: #c8d1d8;
  260. --h5-color: #bbc6ce;
  261. --h6-color: #afbbc4;
  262. --muted-color: #73828c;
  263. --muted-border-color: #1f2d38;
  264. --primary: #1095c1;
  265. --primary-hover: #1ab3e6;
  266. --primary-focus: rgba(16, 149, 193, .25);
  267. --primary-inverse: #fff;
  268. --secondary: #596b78;
  269. --secondary-hover: #73828c;
  270. --secondary-focus: rgba(115, 130, 140, .25);
  271. --secondary-inverse: #fff;
  272. --contrast: #edf0f3;
  273. --contrast-hover: #fff;
  274. --contrast-focus: rgba(115, 130, 140, .25);
  275. --contrast-inverse: #000;
  276. --mark-background-color: #d1c284;
  277. --mark-color: #11191f;
  278. --modal-overlay-background-color: rgba(36, 51, 62, .9);
  279. --loading-spinner-opacity: 0.5;
  280. --icon-folder: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='%23bbc6ce' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z'/%3E%3C/svg%3E");
  281. --icon-file: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23bbc6ce' stroke-width='1.5' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19.5 14.25v-2.63a3.38 3.38 0 0 0-3.38-3.37h-1.5a1.13 1.13 0 0 1-1.12-1.13v-1.5a3.38 3.38 0 0 0-3.38-3.37H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.62c-.62 0-1.12.5-1.12 1.13v17.25c0 .62.5 1.12 1.13 1.12h12.75c.62 0 1.12-.5 1.12-1.13v-9.37a9 9 0 0 0-9-9Z'/%3E%3C/svg%3E");
  282. --icon-folder-open: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23bbc6ce' stroke-width='1.5' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M3.75 9.78c.11-.02.23-.03.34-.03h15.82c.11 0 .23 0 .34.03m-16.5 0a2.25 2.25 0 0 0-1.88 2.54l.85 6a2.25 2.25 0 0 0 2.23 1.93h14.1a2.25 2.25 0 0 0 2.23-1.93l.85-6a2.25 2.25 0 0 0-1.88-2.54m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.88a1.5 1.5 0 0 1 1.06.44l2.12 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.78'/%3E%3C/svg%3E");
  283. color-scheme: dark
  284. }
  285. }
  286. *,
  287. :after,
  288. :before {
  289. background-repeat: no-repeat;
  290. box-sizing: border-box
  291. }
  292. :after,
  293. :before {
  294. text-decoration: inherit;
  295. vertical-align: inherit
  296. }
  297. :where(:root) {
  298. -webkit-tap-highlight-color: transparent;
  299. -webkit-text-size-adjust: 100%;
  300. -moz-text-size-adjust: 100%;
  301. text-size-adjust: 100%;
  302. background-color: var(--background-color);
  303. color: var(--color);
  304. cursor: default;
  305. font-family: var(--font-family);
  306. font-size: var(--font-size);
  307. font-weight: var(--font-weight);
  308. line-height: var(--line-height);
  309. overflow-wrap: break-word;
  310. -moz-tab-size: 4;
  311. -o-tab-size: 4;
  312. tab-size: 4;
  313. text-rendering: optimizeLegibility
  314. }
  315. main {
  316. display: block
  317. }
  318. body {
  319. margin: 0;
  320. width: 100%
  321. }
  322. body>footer,
  323. body>header,
  324. body>main {
  325. margin-left: auto;
  326. margin-right: auto;
  327. padding: var(--block-spacing-vertical) 0;
  328. width: 100%
  329. }
  330. .container,
  331. .container-fluid {
  332. margin-left: auto;
  333. margin-right: auto;
  334. padding-left: var(--spacing);
  335. padding-right: var(--spacing);
  336. width: 100%
  337. }
  338. @media (min-width:576px) {
  339. .container {
  340. max-width: 510px;
  341. padding-left: 0;
  342. padding-right: 0
  343. }
  344. }
  345. @media (min-width:768px) {
  346. .container {
  347. max-width: 700px
  348. }
  349. }
  350. @media (min-width:992px) {
  351. .container {
  352. max-width: 920px
  353. }
  354. }
  355. @media (min-width:1200px) {
  356. .container {
  357. max-width: 1130px
  358. }
  359. }
  360. section {
  361. margin-bottom: var(--block-spacing-vertical)
  362. }
  363. b,
  364. strong {
  365. font-weight: bolder
  366. }
  367. address,
  368. blockquote,
  369. dl,
  370. figure,
  371. form,
  372. ol,
  373. p,
  374. pre,
  375. table,
  376. ul {
  377. color: var(--color);
  378. font-size: var(--font-size);
  379. font-style: normal;
  380. font-weight: var(--font-weight);
  381. margin-bottom: var(--typography-spacing-vertical);
  382. margin-top: 0
  383. }
  384. [role=link],
  385. a:not(.file-link) {
  386. --color: var(--primary);
  387. --background-color: transparent;
  388. background-color: var(--background-color);
  389. color: var(--color);
  390. outline: none;
  391. -webkit-text-decoration: var(--text-decoration);
  392. text-decoration: var(--text-decoration)
  393. }
  394. [role=link]:is([aria-current], :hover, :active, :focus):not(.file-link),
  395. a:is([aria-current], :hover, :active, :focus):not(.file-link) {
  396. color: var(--primary-hover);
  397. text-decoration: underline
  398. }
  399. [role=link]:focus:not(.file-link),
  400. a:focus:not(.file-link) {
  401. --background-color: var(--primary-focus)
  402. }
  403. h1,
  404. h2,
  405. h3,
  406. h4,
  407. h5,
  408. h6 {
  409. color: var(--color);
  410. font-family: var(--font-family);
  411. font-size: var(--font-size);
  412. font-weight: var(--font-weight);
  413. margin-bottom: var(--typography-spacing-vertical);
  414. margin-top: 0
  415. }
  416. h1 {
  417. --color: var(--h1-color)
  418. }
  419. h2 {
  420. --color: var(--h2-color)
  421. }
  422. h3 {
  423. --color: var(--h3-color)
  424. }
  425. h4 {
  426. --color: var(--h4-color)
  427. }
  428. h5 {
  429. --color: var(--h5-color)
  430. }
  431. h6 {
  432. --color: var(--h6-color)
  433. }
  434. :where(address, blockquote, dl, figure, form, ol, p, pre, table, ul)~:is(h1, h2, h3, h4, h5, h6) {
  435. margin-top: var(--typography-spacing-vertical)
  436. }
  437. p {
  438. margin-bottom: var(--typography-spacing-vertical)
  439. }
  440. small {
  441. font-size: var(--font-size)
  442. }
  443. :where(dl, ol, ul) {
  444. -webkit-padding-start: var(--spacing);
  445. -webkit-padding-end: 0;
  446. padding-left: var(--spacing);
  447. padding-right: 0;
  448. padding-inline-end: 0;
  449. padding-inline-start: var(--spacing)
  450. }
  451. :where(dl, ol, ul) li {
  452. margin-bottom: calc(var(--typography-spacing-vertical)*.25)
  453. }
  454. :where(dl, ol, ul) :is(dl, ol, ul) {
  455. margin: 0;
  456. margin-top: calc(var(--typography-spacing-vertical)*.25)
  457. }
  458. mark {
  459. background-color: var(--mark-background-color);
  460. color: var(--mark-color);
  461. padding: .125rem .25rem;
  462. vertical-align: baseline
  463. }
  464. ::-moz-selection {
  465. background-color: var(--primary-focus)
  466. }
  467. ::selection {
  468. background-color: var(--primary-focus)
  469. }
  470. :where(audio, canvas, iframe, img, svg, video) {
  471. vertical-align: middle
  472. }
  473. img {
  474. border-style: none;
  475. height: auto;
  476. max-width: 100%
  477. }
  478. :where(svg:not([fill])) {
  479. fill: currentColor
  480. }
  481. svg:not(:root) {
  482. overflow: hidden
  483. }
  484. legend {
  485. color: inherit;
  486. max-width: 100%;
  487. padding: 0;
  488. white-space: normal
  489. }
  490. ::-webkit-inner-spin-button,
  491. ::-webkit-outer-spin-button {
  492. height: auto
  493. }
  494. ::-moz-focus-inner {
  495. border-style: none;
  496. padding: 0
  497. }
  498. :-moz-focusring {
  499. outline: none
  500. }
  501. :-moz-ui-invalid {
  502. box-shadow: none
  503. }
  504. ::-ms-expand {
  505. display: none
  506. }
  507. fieldset {
  508. border: 0;
  509. margin: 0;
  510. margin-bottom: var(--spacing);
  511. padding: 0
  512. }
  513. fieldset legend,
  514. label {
  515. display: block;
  516. font-weight: var(--form-label-font-weight, var(--font-weight));
  517. margin-bottom: calc(var(--spacing)*.25)
  518. }
  519. [aria-controls] {
  520. cursor: pointer
  521. }
  522. [aria-disabled=true],
  523. [disabled] {
  524. cursor: not-allowed
  525. }
  526. [aria-hidden=false][hidden] {
  527. display: initial
  528. }
  529. [aria-hidden=false][hidden]:not(:focus) {
  530. clip: rect(0, 0, 0, 0);
  531. position: absolute
  532. }
  533. [tabindex],
  534. a,
  535. area,
  536. button,
  537. input,
  538. label,
  539. select,
  540. summary,
  541. textarea {
  542. -ms-touch-action: manipulation
  543. }
  544. .tree {
  545. --tree-spacing: 1.5rem;
  546. --radius: 10px;
  547. }
  548. .tree a {
  549. border-bottom: 1px solid transparent;
  550. border-color: var(--secondary);
  551. color: var(--color);
  552. text-decoration: none;
  553. transition: all 0.2s ease;
  554. }
  555. .tree a:hover {
  556. border-color: var(--secondary-hover);
  557. color: var(--h3-color);
  558. }
  559. .tree,
  560. .tree ul,
  561. .tree li {
  562. list-style: none;
  563. list-style-type: none;
  564. }
  565. .tree .folder li {
  566. display: block;
  567. position: relative;
  568. padding-left: calc(2 * var(--tree-spacing) - var(--radius) - 2px);
  569. }
  570. .tree ul {
  571. margin-left: calc(var(--radius) - var(--tree-spacing));
  572. padding-left: 0;
  573. }
  574. .tree summary {
  575. display: block;
  576. cursor: pointer;
  577. }
  578. .tree summary::marker,
  579. .tree summary::-webkit-details-marker {
  580. display: none;
  581. }
  582. .tree summary:focus {
  583. outline: none;
  584. }
  585. .tree summary:focus-visible {
  586. outline: 1px dotted #000;
  587. }
  588. .tree li.file::before {
  589. margin-right: 10px;
  590. vertical-align: middle;
  591. height: 20px;
  592. width: 20px;
  593. content: '';
  594. display: inline-block;
  595. background-image: var(--icon-file);
  596. background-position: top;
  597. background-size: 75% auto;
  598. }
  599. .tree li.folder>details>summary::before {
  600. z-index: 1;
  601. margin-right: 10px;
  602. vertical-align: middle;
  603. height: 20px;
  604. width: 20px;
  605. content: '';
  606. display: inline-block;
  607. background-image: var(--icon-folder);
  608. background-position: top;
  609. background-size: 75% auto;
  610. }
  611. .tree li.folder>details[open]>summary::before {
  612. background-image: var(--icon-folder-open);
  613. }
  614. </style>
  615. </head>
  616. <body>
  617. <main class="container">
  618. <h1>Sukka Ruleset Server</h1>
  619. <p>
  620. Made by <a href="https://skk.moe">Sukka</a> | <a href="https://github.com/SukkaW/Surge/">Source @ GitHub</a> | Licensed under <a href="/LICENSE" target="_blank">AGPL-3.0</a>
  621. </p>
  622. <p>Last Build: ${new Date().toISOString()}</p>
  623. <br>
  624. <ul class="tree">
  625. ${treeHtml(tree, 0, closedRootFolders)}
  626. </ul>
  627. </main>
  628. </body>
  629. </html>
  630. `;
  631. }