| author | |
| committer | |
| log | 07f12d340d1763aaa9726ee2381c98a80ad580ab |
| tree | 098c56fc4c7f92c92c8aca1c9b4451a412ecfe0b |
| signature | Commit is signed but in an unrecognized format. |
it was weird. i pressed delete on a subfolder, i think one of the
pages.off folders that i was using. and then, suddenly, nvim on windows
7 decided to delete every file in the directory. they weren't shred off
the space time continuum, but just marked deleted. i had to pay $80 to
get access to a software that could see them. bleh!
just seeing all my work, a little over a week, was pretty heart
shattering. but i remembered that long ago, a close friend said i could
call them whenever i was feeling sad. i finally took them up on that
offer. the first time i've ever called someone for emotional support.
but it's ok. i got it back. and the site framework is better than ever.
i'm gonna commit and push more often. the repo is private anyways.183 files changed, 19727 insertions(+), 0 deletions(-)
.gitignore created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | .clover | |
| 2 | node_modules | |
| 3 |
deno.jsonc created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | { | |
| 2 | "lint": { | |
| 3 | "exclude": ["framework/meta"], // OLD | |
| 4 | "rules": { | |
| 5 | "exclude": [ | |
| 6 | "no-explicit-any" // TODO | |
| 7 | ] | |
| 8 | } | |
| 9 | } | |
| 10 | } |
framework/assets.ts created+99| ... | ... | @@ -0,0 +1,99 @@ |
| 1 | interface Loaded { | |
| 2 | map: BuiltAssetMap; | |
| 3 | buf: Buffer; | |
| 4 | } | |
| 5 | let assets: Loaded | null = null; | |
| 6 | ||
| 7 | export type StaticPageId = string; | |
| 8 | ||
| 9 | export async function reload() { | |
| 10 | const [map, buf] = await Promise.all([ | |
| 11 | fs.readFile(".clover/static.json", "utf8"), | |
| 12 | fs.readFile(".clover/static.blob"), | |
| 13 | ]); | |
| 14 | assets = { | |
| 15 | map: JSON.parse(map), | |
| 16 | buf, | |
| 17 | }; | |
| 18 | } | |
| 19 | ||
| 20 | export async function reloadSync() { | |
| 21 | const map = fs.readFileSync(".clover/static.json", "utf8"); | |
| 22 | const buf = fs.readFileSync(".clover/static.blob"); | |
| 23 | assets = { | |
| 24 | map: JSON.parse(map), | |
| 25 | buf, | |
| 26 | }; | |
| 27 | } | |
| 28 | ||
| 29 | export async function assetMiddleware(c: Context, next: Next) { | |
| 30 | if (!assets) await reload(); | |
| 31 | const asset = assets!.map[c.req.path]; | |
| 32 | if (asset) { | |
| 33 | return assetInner(c, asset, 200); | |
| 34 | } | |
| 35 | return next(); | |
| 36 | } | |
| 37 | ||
| 38 | export async function serveAsset( | |
| 39 | c: Context, | |
| 40 | id: StaticPageId, | |
| 41 | status: StatusCode, | |
| 42 | ) { | |
| 43 | assets ?? await reload(); | |
| 44 | return assetInner(c, assets!.map[id], status); | |
| 45 | } | |
| 46 | ||
| 47 | export function hasAsset(id: string) { | |
| 48 | if (!assets) reloadSync(); | |
| 49 | return assets!.map[id] !== undefined; | |
| 50 | } | |
| 51 | ||
| 52 | export function etagMatches(etag: string, ifNoneMatch: string) { | |
| 53 | return ifNoneMatch === etag || ifNoneMatch.split(/,\s*/).indexOf(etag) > -1; | |
| 54 | } | |
| 55 | ||
| 56 | function subarrayAsset([start, end]: View) { | |
| 57 | return assets!.buf.subarray(start, end); | |
| 58 | } | |
| 59 | ||
| 60 | function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) { | |
| 61 | const ifnonematch = c.req.header("If-None-Match"); | |
| 62 | if (ifnonematch) { | |
| 63 | const etag = asset.headers.ETag; | |
| 64 | if (etagMatches(etag, ifnonematch)) { | |
| 65 | c.res = new Response(null, { | |
| 66 | status: 304, | |
| 67 | statusText: "Not Modified", | |
| 68 | headers: { | |
| 69 | ETag: etag, | |
| 70 | }, | |
| 71 | }); | |
| 72 | return; | |
| 73 | } | |
| 74 | } | |
| 75 | const acceptEncoding = c.req.header("Accept-Encoding") ?? ""; | |
| 76 | let body; | |
| 77 | let headers = asset.headers; | |
| 78 | if (acceptEncoding.includes("zstd") && asset.zstd) { | |
| 79 | body = subarrayAsset(asset.zstd); | |
| 80 | headers = { | |
| 81 | ...asset.headers, | |
| 82 | "Content-Encoding": "zstd", | |
| 83 | }; | |
| 84 | } else if (acceptEncoding.includes("gzip") && asset.gzip) { | |
| 85 | body = subarrayAsset(asset.gzip); | |
| 86 | headers = { | |
| 87 | ...asset.headers, | |
| 88 | "Content-Encoding": "gzip", | |
| 89 | }; | |
| 90 | } else { | |
| 91 | body = subarrayAsset(asset.raw); | |
| 92 | } | |
| 93 | c.res = new Response(body, { headers, status }); | |
| 94 | } | |
| 95 | ||
| 96 | import * as fs from "./fs.ts"; | |
| 97 | import type { Context, Next } from "hono"; | |
| 98 | import type { StatusCode } from "hono/utils/http-status"; | |
| 99 | import type { BuiltAsset, BuiltAssetMap, View } from "./incremental.ts"; |
framework/bundle.ts created+85| ... | ... | @@ -0,0 +1,85 @@ |
| 1 | // This file implements client-side bundling, mostly wrapping esbuild. | |
| 2 | import process from "node:process"; | |
| 3 | const plugins: esbuild.Plugin[] = [ | |
| 4 | // There are currently no plugins needed by 'paperclover.net' | |
| 5 | ]; | |
| 6 | ||
| 7 | export async function bundleClientJavaScript( | |
| 8 | referencedScripts: string[], | |
| 9 | extraPublicScripts: string[], | |
| 10 | incr: Incremental, | |
| 11 | dev: boolean = false, | |
| 12 | ) { | |
| 13 | const entryPoints = [ | |
| 14 | ...new Set([ | |
| 15 | ...referencedScripts, | |
| 16 | ...extraPublicScripts, | |
| 17 | ]), | |
| 18 | ]; | |
| 19 | if (entryPoints.length === 0) return; | |
| 20 | const invalidFiles = entryPoints | |
| 21 | .filter((file) => !file.match(/\.client\.[tj]sx?/)); | |
| 22 | if (invalidFiles.length > 0) { | |
| 23 | const cwd = process.cwd(); | |
| 24 | throw new Error( | |
| 25 | "All client-side scripts should be named like '.client.ts'. Exceptions: " + | |
| 26 | invalidFiles.map((x) => path.join(cwd, x)).join(","), | |
| 27 | ); | |
| 28 | } | |
| 29 | ||
| 30 | const bundle = await esbuild.build({ | |
| 31 | bundle: true, | |
| 32 | chunkNames: "/js/c.[hash]", | |
| 33 | entryNames: "/js/[name]", | |
| 34 | assetNames: "/asset/[hash]", | |
| 35 | entryPoints, | |
| 36 | format: "esm", | |
| 37 | minify: !dev, | |
| 38 | outdir: "/out!", | |
| 39 | plugins, | |
| 40 | splitting: true, | |
| 41 | write: false, | |
| 42 | }); | |
| 43 | if (bundle.errors.length || bundle.warnings.length) { | |
| 44 | throw new AggregateError( | |
| 45 | bundle.errors.concat(bundle.warnings), | |
| 46 | "JS bundle failed", | |
| 47 | ); | |
| 48 | } | |
| 49 | incr.invalidate("bundle-script"); | |
| 50 | const publicScriptRoutes = extraPublicScripts.map((file) => | |
| 51 | path.basename(file).replace(/\.client\.[tj]sx?/, "") | |
| 52 | ); | |
| 53 | const promises: Promise<unknown>[] = []; | |
| 54 | // TODO: add a shared build hash to entrypoints, derived from all the chunk hashes. | |
| 55 | for (const file of bundle.outputFiles) { | |
| 56 | let route = file.path.replace(/^.*!/, "").replaceAll("\\", "/"); | |
| 57 | const text = file.text; | |
| 58 | // Register non-chunks as script entries. | |
| 59 | const chunk = route.startsWith("/js/c."); | |
| 60 | if (!chunk) { | |
| 61 | route = route.replace(".client.js", ".js"); | |
| 62 | incr.put({ | |
| 63 | srcId: "bundle-script", | |
| 64 | type: "script", | |
| 65 | key: route.slice("/js/".length, -".js".length), | |
| 66 | value: text, | |
| 67 | }); | |
| 68 | } | |
| 69 | if (chunk || publicScriptRoutes.includes(route)) { | |
| 70 | promises.push(incr.putAsset({ | |
| 71 | srcId: "bundle-script", | |
| 72 | key: route, | |
| 73 | body: text, | |
| 74 | })); | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | if (promises.length > 0) { | |
| 79 | await Promise.all(promises); | |
| 80 | } | |
| 81 | } | |
| 82 | ||
| 83 | import * as path from "node:path"; | |
| 84 | import * as esbuild from "esbuild"; | |
| 85 | import { Incremental } from "./incremental.ts"; |
framework/css.ts created+87| ... | ... | @@ -0,0 +1,87 @@ |
| 1 | export interface Theme { | |
| 2 | bg: string; | |
| 3 | fg: string; | |
| 4 | primary?: string; | |
| 5 | h1?: string; | |
| 6 | } | |
| 7 | ||
| 8 | export function stringifyTheme(theme: Theme) { | |
| 9 | return [ | |
| 10 | ":root {", | |
| 11 | "--bg: " + theme.bg + ";", | |
| 12 | "--fg: " + theme.fg + ";", | |
| 13 | theme.primary ? "--primary: " + theme.primary + ";" : null, | |
| 14 | "}", | |
| 15 | theme.h1 ? "h1 { color: " + theme.h1 + "}" : null, | |
| 16 | ].filter(Boolean).join("\n"); | |
| 17 | } | |
| 18 | ||
| 19 | export function preprocess(css: string, theme: Theme): string { | |
| 20 | const keys = Object.keys(theme); | |
| 21 | const regex = new RegExp( | |
| 22 | `([{};\\n][^{};\\n]*var\\(--(${keys.join("|")})\\).*?(?=[;{}\\n]))`, | |
| 23 | "gs", | |
| 24 | ); | |
| 25 | const regex2 = new RegExp(`var\\(--(${keys.join("|")})\\)`); | |
| 26 | return css.replace( | |
| 27 | regex, | |
| 28 | (_, line) => | |
| 29 | line.replace(regex2, (_: string, varName: string) => theme[varName]) + | |
| 30 | ";" + line.slice(1), | |
| 31 | ); | |
| 32 | } | |
| 33 | ||
| 34 | export async function bundleCssFiles( | |
| 35 | cssImports: string[], | |
| 36 | theme: Theme, | |
| 37 | dev: boolean = false, | |
| 38 | ): Promise<string> { | |
| 39 | const plugin = { | |
| 40 | name: "clover", | |
| 41 | setup(b) { | |
| 42 | b.onResolve( | |
| 43 | { filter: /^\$input\$$/ }, | |
| 44 | () => ({ path: ".", namespace: "input" }), | |
| 45 | ); | |
| 46 | b.onLoad( | |
| 47 | { filter: /./, namespace: "input" }, | |
| 48 | () => ({ | |
| 49 | loader: "css", | |
| 50 | contents: | |
| 51 | cssImports.map((path) => `@import url(${JSON.stringify(path)});`) | |
| 52 | .join("\n") + stringifyTheme(theme), | |
| 53 | resolveDir: ".", | |
| 54 | }), | |
| 55 | ); | |
| 56 | b.onLoad( | |
| 57 | { filter: /\.css$/ }, | |
| 58 | async ({ path: file }) => ({ | |
| 59 | loader: "css", | |
| 60 | contents: preprocess(await fs.readFile(file, "utf-8"), theme), | |
| 61 | }), | |
| 62 | ); | |
| 63 | }, | |
| 64 | } satisfies Plugin; | |
| 65 | const build = await esbuild.build({ | |
| 66 | bundle: true, | |
| 67 | entryPoints: ["$input$"], | |
| 68 | write: false, | |
| 69 | external: ["*.woff2"], | |
| 70 | target: ["ie11"], | |
| 71 | plugins: [plugin], | |
| 72 | minify: !dev, | |
| 73 | }); | |
| 74 | const { errors, warnings, outputFiles } = build; | |
| 75 | if (errors.length > 0) { | |
| 76 | throw new AggregateError(errors, "CSS Build Failed"); | |
| 77 | } | |
| 78 | if (warnings.length > 0) { | |
| 79 | throw new AggregateError(warnings, "CSS Build Failed"); | |
| 80 | } | |
| 81 | if (outputFiles.length > 1) throw new Error("Too many output files"); | |
| 82 | return outputFiles[0].text; | |
| 83 | } | |
| 84 | ||
| 85 | import type { Plugin } from "esbuild"; | |
| 86 | import * as esbuild from "esbuild"; | |
| 87 | import * as fs from "./fs.ts"; |
framework/fs.ts created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | // default | |
| 2 | function isScope(node, parent) { | |
| 3 | if ((0, _index.isBlockStatement)(node) && ((0, _index.isFunction)(parent) || (0, _index.isCatchClause)(parent))) { | |
| 4 | return false; | |
| 5 | } | |
| 6 | if ((0, _index.isPattern)(node) && ((0, _index.isFunction)(parent) || (0, _index.isCatchClause)(parent))) { | |
| 7 | return true; | |
| 8 | } | |
| 9 | return (0, _index.isScopable)(node); | |
| 10 | } |
framework/hot.ts created+240| ... | ... | @@ -0,0 +1,240 @@ |
| 1 | // This implements the ability to use TS, TSX, and more plugins | |
| 2 | // in Node.js. It cannot be built on the ES module loader, | |
| 3 | // because there is no exposed way to replace modules when | |
| 4 | // needed (see nodejs#49442). | |
| 5 | // | |
| 6 | // It also allows using a simple compile cache, which is used by | |
| 7 | // the site generator to determine when code changes. | |
| 8 | export const projectRoot = path.resolve(import.meta.dirname, "../"); | |
| 9 | export const projectSrc = path.resolve(projectRoot, "src"); | |
| 10 | ||
| 11 | // Create a project-relative require. For convenience, it is generic-typed. | |
| 12 | export const load = createRequire( | |
| 13 | pathToFileURL(path.join(projectRoot, "run.js")).toString(), | |
| 14 | ) as { | |
| 15 | <T = unknown>(id: string): T; | |
| 16 | extensions: NodeJS.Dict<(mod: NodeJS.Module, file: string) => unknown>; | |
| 17 | cache: NodeJS.Dict<NodeJS.Module>; | |
| 18 | resolve: (id: string, o?: { paths: string[] }) => string; | |
| 19 | }; | |
| 20 | export const { cache } = load; | |
| 21 | ||
| 22 | // Register extensions by overwriting `require.extensions` | |
| 23 | const require = load; | |
| 24 | const exts = require.extensions; | |
| 25 | exts[".ts"] = loadEsbuild; | |
| 26 | exts[".tsx"] = loadEsbuild; | |
| 27 | exts[".jsx"] = loadEsbuild; | |
| 28 | exts[".marko"] = loadMarko; | |
| 29 | exts[".mdx"] = loadMdx; | |
| 30 | exts[".css"] = loadCss; | |
| 31 | ||
| 32 | // Intercept all module load calls to track CSS imports + file times. | |
| 33 | export interface FileStat { | |
| 34 | cssImportsRecursive: string[] | null; | |
| 35 | lastModified: number; | |
| 36 | imports: string[]; | |
| 37 | /* Used by 'incremental.ts' */ | |
| 38 | srcIds: string[]; | |
| 39 | } | |
| 40 | let fsGraph = new Map<string, FileStat>(); | |
| 41 | export function setFsGraph(g: Map<string, FileStat>) { | |
| 42 | if (fsGraph.size > 0) { | |
| 43 | throw new Error("Cannot restore fsGraph when it has been written into"); | |
| 44 | } | |
| 45 | fsGraph = g; | |
| 46 | } | |
| 47 | export function getFsGraph() { | |
| 48 | return fsGraph; | |
| 49 | } | |
| 50 | ||
| 51 | function shouldTrackPath(filename: string) { | |
| 52 | return !filename.includes("node_modules") && | |
| 53 | !filename.includes(import.meta.dirname); | |
| 54 | } | |
| 55 | ||
| 56 | const Module = load<typeof import("node:module")>("node:module"); | |
| 57 | const ModulePrototypeUnderscoreCompile = Module.prototype._compile; | |
| 58 | Module.prototype._compile = function ( | |
| 59 | content: string, | |
| 60 | filename: string, | |
| 61 | format: "module" | "commonjs", | |
| 62 | ) { | |
| 63 | fs.writeMkdirSync( | |
| 64 | ".clover/debug-transpilation/" + | |
| 65 | path.relative(projectRoot, filename).replaceAll("\\", "/").replaceAll( | |
| 66 | "../", | |
| 67 | "_/", | |
| 68 | ).replaceAll("/", "."), | |
| 69 | content, | |
| 70 | ); | |
| 71 | const result = ModulePrototypeUnderscoreCompile.call( | |
| 72 | this, | |
| 73 | content, | |
| 74 | filename, | |
| 75 | format, | |
| 76 | ); | |
| 77 | const stat = fs.statSync(filename); | |
| 78 | if (shouldTrackPath(filename)) { | |
| 79 | const cssImportsMaybe: string[] = []; | |
| 80 | const imports: string[] = []; | |
| 81 | for (const { filename: file } of this.children) { | |
| 82 | const relative = path.relative(projectRoot, file); | |
| 83 | if (file.endsWith(".css")) cssImportsMaybe.push(relative); | |
| 84 | else { | |
| 85 | const child = fsGraph.get(relative); | |
| 86 | if (!child) continue; | |
| 87 | const { cssImportsRecursive } = child; | |
| 88 | if (cssImportsRecursive) cssImportsMaybe.push(...cssImportsRecursive); | |
| 89 | imports.push(relative); | |
| 90 | } | |
| 91 | } | |
| 92 | const relative = path.relative(projectRoot, filename); | |
| 93 | fsGraph.set(relative, { | |
| 94 | cssImportsRecursive: cssImportsMaybe.length > 0 | |
| 95 | ? Array.from(new Set(cssImportsMaybe)) | |
| 96 | : null, | |
| 97 | imports, | |
| 98 | lastModified: stat.mtimeMs, | |
| 99 | srcIds: [], | |
| 100 | }); | |
| 101 | } | |
| 102 | return result; | |
| 103 | }; | |
| 104 | ||
| 105 | // Implement @/ prefix | |
| 106 | const ModuleUnderscoreResolveFilename = Module._resolveFilename; | |
| 107 | Module._resolveFilename = (...args) => { | |
| 108 | if (args[0].startsWith("@/")) { | |
| 109 | const replacedPath = "." + args[0].slice(1); | |
| 110 | try { | |
| 111 | return require.resolve(replacedPath, { paths: [projectSrc] }); | |
| 112 | } catch (err: any) { | |
| 113 | if (err.code === "MODULE_NOT_FOUND" && err.requireStack.length <= 1) { | |
| 114 | err.message.replace(replacedPath, args[0]); | |
| 115 | } | |
| 116 | } | |
| 117 | } | |
| 118 | return ModuleUnderscoreResolveFilename(...args); | |
| 119 | }; | |
| 120 | ||
| 121 | function loadEsbuild(module: NodeJS.Module, filepath: string) { | |
| 122 | let src = fs.readFileSync(filepath, "utf8"); | |
| 123 | return loadEsbuildCode(module, filepath, src); | |
| 124 | } | |
| 125 | ||
| 126 | function loadEsbuildCode(module: NodeJS.Module, filepath: string, src: string) { | |
| 127 | if (filepath === import.meta.filename) { | |
| 128 | module.exports = self; | |
| 129 | return; | |
| 130 | } | |
| 131 | ||
| 132 | let loader: any = "tsx"; | |
| 133 | if (filepath.endsWith(".ts")) loader = "ts"; | |
| 134 | else if (filepath.endsWith(".jsx")) loader = "jsx"; | |
| 135 | else if (filepath.endsWith(".js")) loader = "js"; | |
| 136 | if (src.includes("import.meta")) { | |
| 137 | src = ` | |
| 138 | import.meta.url = ${JSON.stringify(pathToFileURL(filepath).toString())}; | |
| 139 | import.meta.dirname = ${JSON.stringify(path.dirname(filepath))}; | |
| 140 | import.meta.filename = ${JSON.stringify(filepath)}; | |
| 141 | ` + src; | |
| 142 | } | |
| 143 | src = esbuild.transformSync(src, { | |
| 144 | loader, | |
| 145 | format: "cjs", | |
| 146 | target: "esnext", | |
| 147 | jsx: "automatic", | |
| 148 | jsxImportSource: "#ssr", | |
| 149 | }).code; | |
| 150 | return module._compile(src, filepath, "commonjs"); | |
| 151 | } | |
| 152 | ||
| 153 | function loadMarko(module: NodeJS.Module, filepath: string) { | |
| 154 | let src = fs.readFileSync(filepath, "utf8"); | |
| 155 | // A non-standard thing here is Clover Sitegen implements | |
| 156 | // its own client side scripting stuff, so it overrides | |
| 157 | // bare client import statements to it's own usage. | |
| 158 | if (src.match(/^\s*client\s+import\s+["']/m)) { | |
| 159 | src = src.replace( | |
| 160 | /^\s*client\s+import\s+("[^"]+|'[^']+)[^\n]+/m, | |
| 161 | "<CloverScriptInclude src=$1 />", | |
| 162 | ) + '\nimport { Script as CloverScriptInclude } from "#sitegen";'; | |
| 163 | } | |
| 164 | ||
| 165 | src = marko.compileSync(filepath, {}).code; | |
| 166 | src = src.replace("marko/debug/html", "#ssr/marko"); | |
| 167 | return loadEsbuildCode(module, filepath, src); | |
| 168 | } | |
| 169 | ||
| 170 | function loadMdx(module: NodeJS.Module, filepath: string) { | |
| 171 | const input = fs.readFileSync(filepath); | |
| 172 | const out = mdx.compileSync(input, { jsxImportSource: "#ssr" }).value; | |
| 173 | const src = typeof out === "string" ? out : Buffer.from(out).toString("utf8"); | |
| 174 | return loadEsbuildCode(module, filepath, src); | |
| 175 | } | |
| 176 | ||
| 177 | function loadCss(module: NodeJS.Module, filepath: string) { | |
| 178 | module.exports = {}; | |
| 179 | } | |
| 180 | ||
| 181 | export function reloadRecursive(filepath: string) { | |
| 182 | filepath = path.resolve(filepath); | |
| 183 | const existing = cache[filepath]; | |
| 184 | if (existing) deleteRecursive(filepath, existing); | |
| 185 | fsGraph.clear(); | |
| 186 | return require(filepath); | |
| 187 | } | |
| 188 | ||
| 189 | function deleteRecursive(id: string, module: any) { | |
| 190 | if (id.includes(path.sep + "node_modules" + path.sep)) { | |
| 191 | return; | |
| 192 | } | |
| 193 | delete cache[id]; | |
| 194 | for (const child of module.children) { | |
| 195 | if (child.filename.includes("/engine/")) return; | |
| 196 | const existing = cache[child.filename]; | |
| 197 | if (existing === child) deleteRecursive(child.filename, existing); | |
| 198 | } | |
| 199 | } | |
| 200 | ||
| 201 | export function getCssImports(filepath: string) { | |
| 202 | filepath = path.resolve(filepath); | |
| 203 | if (!require.cache[filepath]) throw new Error(filepath + " was never loaded"); | |
| 204 | return fsGraph.get(path.relative(projectRoot, filepath)) | |
| 205 | ?.cssImportsRecursive ?? []; | |
| 206 | } | |
| 207 | ||
| 208 | export function resolveFrom(src: string, dest: string) { | |
| 209 | try { | |
| 210 | return createRequire(src).resolve(dest); | |
| 211 | } catch (err: any) { | |
| 212 | if (err.code === "MODULE_NOT_FOUND" && err.requireStack.length <= 1) { | |
| 213 | err.message = err.message.split("\n")[0] + " from '" + src + "'"; | |
| 214 | } | |
| 215 | throw err; | |
| 216 | } | |
| 217 | } | |
| 218 | ||
| 219 | declare global { | |
| 220 | namespace NodeJS { | |
| 221 | interface Module { | |
| 222 | _compile( | |
| 223 | this: NodeJS.Module, | |
| 224 | content: string, | |
| 225 | filepath: string, | |
| 226 | format: "module" | "commonjs", | |
| 227 | ): unknown; | |
| 228 | } | |
| 229 | } | |
| 230 | } | |
| 231 | ||
| 232 | import * as fs from "./fs.ts"; | |
| 233 | import * as path from "node:path"; | |
| 234 | import { pathToFileURL } from "node:url"; | |
| 235 | import * as esbuild from "esbuild"; | |
| 236 | import * as marko from "@marko/compiler"; | |
| 237 | import { createRequire } from "node:module"; | |
| 238 | import * as mdx from "@mdx-js/mdx"; | |
| 239 | import * as self from "./hot.ts"; | |
| 240 | import { Buffer } from "node:buffer"; |
framework/incremental.ts created+265| ... | ... | @@ -0,0 +1,265 @@ |
| 1 | // `Incremental` contains multiple maps for the different parts of a site | |
| 2 | // build, and tracks reused items across builds. It also handles emitting and | |
| 3 | // updating the built site. This structure is self contained and serializable. | |
| 4 | // | |
| 5 | // Tracking is simple: Files map to one or more 'source IDs', which map to one | |
| 6 | // or more 'artifact'. This two layer approach allows many files (say a page + | |
| 7 | // all its imports) to map to the build of a page, which produces an HTML file | |
| 8 | // plus a list of scripts. | |
| 9 | ||
| 10 | import { Buffer } from "node:buffer"; | |
| 11 | interface ArtifactMap { | |
| 12 | asset: Asset; | |
| 13 | script: string; | |
| 14 | } | |
| 15 | type AllArtifactMaps = { | |
| 16 | [K in keyof ArtifactMap]: Map<string, ArtifactMap[K]>; | |
| 17 | }; | |
| 18 | type ArtifactType = keyof ArtifactMap; | |
| 19 | ||
| 20 | interface Asset { | |
| 21 | buffer: Buffer; | |
| 22 | headers: Record<string, string | undefined>; | |
| 23 | hash: string; | |
| 24 | } | |
| 25 | ||
| 26 | export interface PutBase { | |
| 27 | srcTag?: string; // deprecated | |
| 28 | srcId: string; | |
| 29 | key: string; | |
| 30 | } | |
| 31 | ||
| 32 | export interface Put<T extends ArtifactType> extends PutBase { | |
| 33 | type: T; | |
| 34 | value: ArtifactMap[T]; | |
| 35 | } | |
| 36 | ||
| 37 | export interface Output { | |
| 38 | type: ArtifactType; | |
| 39 | key: string; | |
| 40 | } | |
| 41 | ||
| 42 | const gzip = util.promisify(zlib.gzip); | |
| 43 | const zstd = util.promisify(zlib.zstdCompress); | |
| 44 | ||
| 45 | export class Incremental { | |
| 46 | /** The generated artifacts */ | |
| 47 | out: AllArtifactMaps = { | |
| 48 | asset: new Map(), | |
| 49 | script: new Map(), | |
| 50 | }; | |
| 51 | /** Compressed resources */ | |
| 52 | compress = new Map<string, Compressed>(); | |
| 53 | compressQueue = new Queue<CompressJob, void>({ | |
| 54 | name: "Compress", | |
| 55 | maxJobs: 5, | |
| 56 | fn: this.compressImpl.bind(this), | |
| 57 | passive: true, | |
| 58 | getItemText: (job) => `${job.algo.toUpperCase()} ${job.label}`, | |
| 59 | }); | |
| 60 | /** Tracking filesystem entries to `srcId` */ | |
| 61 | files = new Map<string, hot.FileStat>(); | |
| 62 | srcIds = new Map<string, Output[]>(); | |
| 63 | ||
| 64 | static fromSerialized() { | |
| 65 | } | |
| 66 | serialize() { | |
| 67 | const writer = new BufferWriter(); | |
| 68 | ||
| 69 | const asset = Array.from( | |
| 70 | this.out.asset, | |
| 71 | ([key, { buffer, hash, headers }]) => { | |
| 72 | const raw = writer.write(buffer, hash); | |
| 73 | const { gzip: gzipBuf, zstd: zstdBuf } = this.compress.get(hash) ?? {}; | |
| 74 | const gzip = gzipBuf ? writer.write(gzipBuf, hash) : null; | |
| 75 | const zstd = zstdBuf ? writer.write(zstdBuf, hash) : null; | |
| 76 | return [key, { | |
| 77 | raw, | |
| 78 | gzip, | |
| 79 | zstd, | |
| 80 | hash, | |
| 81 | headers, | |
| 82 | }]; | |
| 83 | }, | |
| 84 | ); | |
| 85 | const script = Array.from(this.out.script); | |
| 86 | ||
| 87 | const meta = Buffer.from( | |
| 88 | JSON.stringify({ | |
| 89 | asset, | |
| 90 | script, | |
| 91 | }), | |
| 92 | "utf-8", | |
| 93 | ); | |
| 94 | ||
| 95 | const lengthBuffer = Buffer.alloc(4); | |
| 96 | lengthBuffer.writeUInt32LE(meta.byteLength, 0); | |
| 97 | ||
| 98 | return Buffer.concat([meta, lengthBuffer, ...writer.buffers]); | |
| 99 | } | |
| 100 | ||
| 101 | serializeToDisk(file = ".clover/incr.state") { | |
| 102 | const buffer = this.serialize(); | |
| 103 | fs.writeFileSync(file, buffer); | |
| 104 | } | |
| 105 | ||
| 106 | put<T extends ArtifactType>({ | |
| 107 | srcId, | |
| 108 | type, | |
| 109 | key, | |
| 110 | value, | |
| 111 | }: Put<T>) { | |
| 112 | this.out[type].set(key, value); | |
| 113 | } | |
| 114 | ||
| 115 | async putAsset(info: PutAsset) { | |
| 116 | const { body, headers, key } = info; | |
| 117 | const buffer = Buffer.isBuffer(body) ? body : Buffer.from(body); | |
| 118 | const hash = Buffer.from(await crypto.subtle.digest("sha-1", buffer)) | |
| 119 | .toString("hex"); | |
| 120 | const value: Asset = { | |
| 121 | buffer, | |
| 122 | headers: { | |
| 123 | "Content-Type": headers?.["Content-Type"] ?? mime.contentTypeFor(key), | |
| 124 | "ETag": JSON.stringify(hash), | |
| 125 | ...headers, | |
| 126 | }, | |
| 127 | hash, | |
| 128 | }; | |
| 129 | if (!this.compress.has(hash)) { | |
| 130 | const label = info.key; | |
| 131 | this.compress.set(hash, { | |
| 132 | zstd: undefined, | |
| 133 | gzip: undefined, | |
| 134 | }); | |
| 135 | await Promise.all([ | |
| 136 | this.compressQueue.add({ label, buffer, algo: "zstd", hash }), | |
| 137 | this.compressQueue.add({ label, buffer, algo: "gzip", hash }), | |
| 138 | ]); | |
| 139 | } | |
| 140 | return this.put({ ...info, type: "asset", value }); | |
| 141 | } | |
| 142 | ||
| 143 | async compressImpl({ algo, buffer, hash }: CompressJob) { | |
| 144 | let out; | |
| 145 | switch (algo) { | |
| 146 | case "zstd": | |
| 147 | out = await zstd(buffer); | |
| 148 | break; | |
| 149 | case "gzip": | |
| 150 | out = await gzip(buffer, { level: 9 }); | |
| 151 | break; | |
| 152 | } | |
| 153 | let entry = this.compress.get(hash); | |
| 154 | if (!entry) { | |
| 155 | this.compress.set( | |
| 156 | hash, | |
| 157 | entry = { | |
| 158 | zstd: undefined, | |
| 159 | gzip: undefined, | |
| 160 | }, | |
| 161 | ); | |
| 162 | } | |
| 163 | entry![algo] = out; | |
| 164 | } | |
| 165 | ||
| 166 | invalidate(srcId: string) { | |
| 167 | } | |
| 168 | ||
| 169 | async wait() { | |
| 170 | await this.compressQueue.done({ method: "stop" }); | |
| 171 | } | |
| 172 | async flush() { | |
| 173 | const writer = new BufferWriter(); | |
| 174 | const asset = Object.fromEntries( | |
| 175 | Array.from(this.out.asset, ([key, { buffer, hash, headers }]) => { | |
| 176 | const raw = writer.write(buffer, hash); | |
| 177 | const { gzip: gzipBuf, zstd: zstdBuf } = this.compress.get(hash) ?? {}; | |
| 178 | const gzip = gzipBuf ? writer.write(gzipBuf, hash) : null; | |
| 179 | const zstd = zstdBuf ? writer.write(zstdBuf, hash) : null; | |
| 180 | return [key, { | |
| 181 | raw, | |
| 182 | gzip, | |
| 183 | zstd, | |
| 184 | headers, | |
| 185 | }]; | |
| 186 | }), | |
| 187 | ); | |
| 188 | await Promise.all([ | |
| 189 | fs.writeFile(".clover/static.json", JSON.stringify(asset)), | |
| 190 | fs.writeFile(".clover/static.blob", writer.get()), | |
| 191 | ]); | |
| 192 | } | |
| 193 | } | |
| 194 | ||
| 195 | export interface PutAsset extends PutBase { | |
| 196 | body: string | Buffer; | |
| 197 | headers?: Record<string, string | undefined>; | |
| 198 | } | |
| 199 | ||
| 200 | export interface Compressed { | |
| 201 | gzip?: Buffer; | |
| 202 | zstd?: Buffer; | |
| 203 | } | |
| 204 | ||
| 205 | export interface CompressJob { | |
| 206 | algo: "zstd" | "gzip"; | |
| 207 | buffer: Buffer; | |
| 208 | label: string; | |
| 209 | hash: string; | |
| 210 | } | |
| 211 | ||
| 212 | class BufferWriter { | |
| 213 | size = 0; | |
| 214 | seen = new Map<string, View>(); | |
| 215 | buffers: Buffer[] = []; | |
| 216 | ||
| 217 | write(buffer: Buffer, hash: string): View { | |
| 218 | let view = this.seen.get(hash); | |
| 219 | if (view) return view; | |
| 220 | view = [this.size, this.size += buffer.byteLength]; | |
| 221 | this.seen.set(hash, view); | |
| 222 | this.buffers.push(buffer); | |
| 223 | return view; | |
| 224 | } | |
| 225 | ||
| 226 | get() { | |
| 227 | return Buffer.concat(this.buffers); | |
| 228 | } | |
| 229 | } | |
| 230 | ||
| 231 | export type View = [start: number, end: number]; | |
| 232 | ||
| 233 | // Alongside this type is a byte buffer, containing all the assets. | |
| 234 | export interface BuiltAssetMap { | |
| 235 | [route: string]: BuiltAsset; | |
| 236 | } | |
| 237 | export interface BuiltAsset { | |
| 238 | raw: View; | |
| 239 | gzip: View; | |
| 240 | zstd: View; | |
| 241 | headers: Record<string, string>; | |
| 242 | } | |
| 243 | ||
| 244 | export interface SerializedMeta { | |
| 245 | asset: Array<[route: string, data: { | |
| 246 | raw: View; | |
| 247 | gzip: View | null; | |
| 248 | zstd: View | null; | |
| 249 | hash: string; | |
| 250 | headers: Record<string, string>; | |
| 251 | }]>; | |
| 252 | script: [key: string, value: string][]; | |
| 253 | } | |
| 254 | ||
| 255 | function never(): never { | |
| 256 | throw new Error("Impossible"); | |
| 257 | } | |
| 258 | ||
| 259 | import * as path from "node:path"; | |
| 260 | import * as fs from "./fs.ts"; | |
| 261 | import * as zlib from "node:zlib"; | |
| 262 | import * as util from "node:util"; | |
| 263 | import { Queue } from "./queue.ts"; | |
| 264 | import * as hot from "./hot.ts"; | |
| 265 | import * as mime from "./mime.ts"; |
framework/meta/index.ts created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | import { resolveMetadata } from "./merge"; | |
| 2 | import { renderMetadata } from "./render"; | |
| 3 | import { Metadata } from "./types"; | |
| 4 | ||
| 5 | export * from "./types"; | |
| 6 | export * from "./merge"; | |
| 7 | export * from "./render"; | |
| 8 | ||
| 9 | export function resolveAndRenderMetadata( | |
| 10 | ...metadata: [Metadata, ...Metadata[]] | |
| 11 | ) { | |
| 12 | return renderMetadata(resolveMetadata(...metadata)); | |
| 13 | } |
framework/meta/merge.ts created+154| ... | ... | @@ -0,0 +1,154 @@ |
| 1 | import { createDefaultMetadata } from "./nextjs/default-metadata"; | |
| 2 | import { resolveAsArrayOrUndefined } from "./nextjs/generate/utils"; | |
| 3 | import { | |
| 4 | resolveAlternates, | |
| 5 | resolveAppleWebApp, | |
| 6 | resolveAppLinks, | |
| 7 | resolveRobots, | |
| 8 | resolveThemeColor, | |
| 9 | resolveVerification, | |
| 10 | resolveViewport, | |
| 11 | } from "./nextjs/resolvers/resolve-basics"; | |
| 12 | import { resolveIcons } from "./nextjs/resolvers/resolve-icons"; | |
| 13 | import { | |
| 14 | resolveOpenGraph, | |
| 15 | resolveTwitter, | |
| 16 | } from "./nextjs/resolvers/resolve-opengraph"; | |
| 17 | import { resolveTitle } from "./nextjs/resolvers/resolve-title"; | |
| 18 | import type { | |
| 19 | Metadata, | |
| 20 | ResolvedMetadata, | |
| 21 | } from "./nextjs/types/metadata-interface"; | |
| 22 | ||
| 23 | type MetadataAccumulationOptions = { | |
| 24 | pathname: string; | |
| 25 | }; | |
| 26 | ||
| 27 | // Merge the source metadata into the resolved target metadata. | |
| 28 | function merge( | |
| 29 | target: ResolvedMetadata, | |
| 30 | source: Metadata | null, | |
| 31 | titleTemplates: { | |
| 32 | title?: string | null; | |
| 33 | twitter?: string | null; | |
| 34 | openGraph?: string | null; | |
| 35 | } = {}, | |
| 36 | ) { | |
| 37 | const metadataBase = source?.metadataBase || target.metadataBase; | |
| 38 | for (const key_ in source) { | |
| 39 | const key = key_ as keyof Metadata; | |
| 40 | ||
| 41 | switch (key) { | |
| 42 | case "title": { | |
| 43 | target.title = resolveTitle(source.title, titleTemplates.title); | |
| 44 | break; | |
| 45 | } | |
| 46 | case "alternates": { | |
| 47 | target.alternates = resolveAlternates(source.alternates, metadataBase, { | |
| 48 | pathname: (source as any)._pathname ?? "/", | |
| 49 | }); | |
| 50 | break; | |
| 51 | } | |
| 52 | case "openGraph": { | |
| 53 | target.openGraph = resolveOpenGraph(source.openGraph, metadataBase); | |
| 54 | if (target.openGraph) { | |
| 55 | target.openGraph.title = resolveTitle( | |
| 56 | target.openGraph.title, | |
| 57 | titleTemplates.openGraph, | |
| 58 | ); | |
| 59 | } | |
| 60 | break; | |
| 61 | } | |
| 62 | case "twitter": { | |
| 63 | target.twitter = resolveTwitter(source.twitter, metadataBase); | |
| 64 | if (target.twitter) { | |
| 65 | target.twitter.title = resolveTitle( | |
| 66 | target.twitter.title, | |
| 67 | titleTemplates.twitter, | |
| 68 | ); | |
| 69 | } | |
| 70 | break; | |
| 71 | } | |
| 72 | case "verification": | |
| 73 | target.verification = resolveVerification(source.verification); | |
| 74 | break; | |
| 75 | case "viewport": { | |
| 76 | target.viewport = resolveViewport(source.viewport); | |
| 77 | break; | |
| 78 | } | |
| 79 | case "icons": { | |
| 80 | target.icons = resolveIcons(source.icons); | |
| 81 | break; | |
| 82 | } | |
| 83 | case "appleWebApp": | |
| 84 | target.appleWebApp = resolveAppleWebApp(source.appleWebApp); | |
| 85 | break; | |
| 86 | case "appLinks": | |
| 87 | target.appLinks = resolveAppLinks(source.appLinks); | |
| 88 | break; | |
| 89 | case "robots": { | |
| 90 | target.robots = resolveRobots(source.robots); | |
| 91 | break; | |
| 92 | } | |
| 93 | case "themeColor": { | |
| 94 | target.themeColor = resolveThemeColor(source.themeColor); | |
| 95 | break; | |
| 96 | } | |
| 97 | case "archives": | |
| 98 | case "assets": | |
| 99 | case "bookmarks": | |
| 100 | case "keywords": | |
| 101 | case "authors": { | |
| 102 | // FIXME: type inferring | |
| 103 | // @ts-ignore | |
| 104 | target[key] = resolveAsArrayOrUndefined(source[key]) || null; | |
| 105 | break; | |
| 106 | } | |
| 107 | // directly assign fields that fallback to null | |
| 108 | case "applicationName": | |
| 109 | case "description": | |
| 110 | case "generator": | |
| 111 | case "creator": | |
| 112 | case "publisher": | |
| 113 | case "category": | |
| 114 | case "classification": | |
| 115 | case "referrer": | |
| 116 | case "colorScheme": | |
| 117 | case "itunes": | |
| 118 | case "formatDetection": | |
| 119 | case "manifest": | |
| 120 | // @ts-ignore TODO: support inferring | |
| 121 | target[key] = source[key] || null; | |
| 122 | break; | |
| 123 | case "other": | |
| 124 | target.other = Object.assign({}, target.other, source.other); | |
| 125 | break; | |
| 126 | case "metadataBase": | |
| 127 | target.metadataBase = metadataBase; | |
| 128 | break; | |
| 129 | default: | |
| 130 | break; | |
| 131 | } | |
| 132 | } | |
| 133 | ||
| 134 | return target; | |
| 135 | } | |
| 136 | ||
| 137 | export interface MetadataWithPathname extends Metadata { | |
| 138 | /** Set by framework author to the pathname of the page defining this metadata. */ | |
| 139 | _pathname?: string; | |
| 140 | } | |
| 141 | ||
| 142 | export function resolveMetadata( | |
| 143 | ...metadata: [MetadataWithPathname, ...MetadataWithPathname[]] | |
| 144 | ) { | |
| 145 | const base = createDefaultMetadata(); | |
| 146 | for (const item of metadata) { | |
| 147 | merge(base, item, { | |
| 148 | title: base.title?.template, | |
| 149 | twitter: base.twitter?.title?.template, | |
| 150 | openGraph: base.openGraph?.title?.template, | |
| 151 | }); | |
| 152 | } | |
| 153 | return base; | |
| 154 | } |
framework/meta/nextjs/constants.ts created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | import type { Viewport } from "./types/extra-types"; | |
| 2 | import type { Icons } from "./types/metadata-types"; | |
| 3 | ||
| 4 | export const ViewPortKeys: { [k in keyof Viewport]: string } = { | |
| 5 | width: "width", | |
| 6 | height: "height", | |
| 7 | initialScale: "initial-scale", | |
| 8 | minimumScale: "minimum-scale", | |
| 9 | maximumScale: "maximum-scale", | |
| 10 | viewportFit: "viewport-fit", | |
| 11 | userScalable: "user-scalable", | |
| 12 | interactiveWidget: "interactive-widget", | |
| 13 | } as const; | |
| 14 | ||
| 15 | export const IconKeys: (keyof Icons)[] = ["icon", "shortcut", "apple", "other"]; |
framework/meta/nextjs/default-metadata.ts created+50| ... | ... | @@ -0,0 +1,50 @@ |
| 1 | import type { ResolvedMetadata } from "./types/metadata-interface"; | |
| 2 | import process from "node:process"; | |
| 3 | ||
| 4 | export function createDefaultMetadata(): ResolvedMetadata { | |
| 5 | const defaultMetadataBase = | |
| 6 | process.env.NODE_ENV === "production" && process.env.VERCEL_URL | |
| 7 | ? new URL(`https://${process.env.VERCEL_URL}`) | |
| 8 | : null; | |
| 9 | ||
| 10 | return { | |
| 11 | viewport: "width=device-width, initial-scale=1", | |
| 12 | metadataBase: defaultMetadataBase, | |
| 13 | ||
| 14 | // Other values are all null | |
| 15 | title: null, | |
| 16 | description: null, | |
| 17 | applicationName: null, | |
| 18 | authors: null, | |
| 19 | generator: null, | |
| 20 | keywords: null, | |
| 21 | referrer: null, | |
| 22 | themeColor: null, | |
| 23 | colorScheme: null, | |
| 24 | creator: null, | |
| 25 | publisher: null, | |
| 26 | robots: null, | |
| 27 | manifest: null, | |
| 28 | alternates: { | |
| 29 | canonical: null, | |
| 30 | languages: null, | |
| 31 | media: null, | |
| 32 | types: null, | |
| 33 | }, | |
| 34 | icons: null, | |
| 35 | openGraph: null, | |
| 36 | twitter: null, | |
| 37 | verification: {}, | |
| 38 | appleWebApp: null, | |
| 39 | formatDetection: null, | |
| 40 | itunes: null, | |
| 41 | abstract: null, | |
| 42 | appLinks: null, | |
| 43 | archives: null, | |
| 44 | assets: null, | |
| 45 | bookmarks: null, | |
| 46 | category: null, | |
| 47 | classification: null, | |
| 48 | other: {}, | |
| 49 | }; | |
| 50 | } |
framework/meta/nextjs/generate/alternate.tsx created+72| ... | ... | @@ -0,0 +1,72 @@ |
| 1 | import type { ResolvedMetadata } from "../types/metadata-interface"; | |
| 2 | ||
| 3 | import React from "react"; | |
| 4 | import { AlternateLinkDescriptor } from "../types/alternative-urls-types"; | |
| 5 | ||
| 6 | function AlternateLink({ | |
| 7 | descriptor, | |
| 8 | ...props | |
| 9 | }: { | |
| 10 | descriptor: AlternateLinkDescriptor; | |
| 11 | } & React.LinkHTMLAttributes<HTMLLinkElement>) { | |
| 12 | if (!descriptor.url) return null; | |
| 13 | return ( | |
| 14 | <link | |
| 15 | {...props} | |
| 16 | {...(descriptor.title && { title: descriptor.title })} | |
| 17 | href={descriptor.url.toString()} | |
| 18 | /> | |
| 19 | ); | |
| 20 | } | |
| 21 | ||
| 22 | export function AlternatesMetadata({ | |
| 23 | alternates, | |
| 24 | }: { | |
| 25 | alternates: ResolvedMetadata["alternates"]; | |
| 26 | }) { | |
| 27 | if (!alternates) return null; | |
| 28 | const { canonical, languages, media, types } = alternates; | |
| 29 | return ( | |
| 30 | <> | |
| 31 | {canonical | |
| 32 | ? <AlternateLink rel="canonical" descriptor={canonical} /> | |
| 33 | : null} | |
| 34 | {languages | |
| 35 | ? Object.entries(languages).map(([locale, descriptors]) => { | |
| 36 | return descriptors?.map((descriptor, index) => ( | |
| 37 | <AlternateLink | |
| 38 | rel="alternate" | |
| 39 | key={index} | |
| 40 | hrefLang={locale} | |
| 41 | descriptor={descriptor} | |
| 42 | /> | |
| 43 | )); | |
| 44 | }) | |
| 45 | : null} | |
| 46 | {media | |
| 47 | ? Object.entries(media).map(([mediaName, descriptors]) => | |
| 48 | descriptors?.map((descriptor, index) => ( | |
| 49 | <AlternateLink | |
| 50 | rel="alternate" | |
| 51 | key={index} | |
| 52 | media={mediaName} | |
| 53 | descriptor={descriptor} | |
| 54 | /> | |
| 55 | )) | |
| 56 | ) | |
| 57 | : null} | |
| 58 | {types | |
| 59 | ? Object.entries(types).map(([type, descriptors]) => | |
| 60 | descriptors?.map((descriptor, index) => ( | |
| 61 | <AlternateLink | |
| 62 | rel="alternate" | |
| 63 | key={index} | |
| 64 | type={type} | |
| 65 | descriptor={descriptor} | |
| 66 | /> | |
| 67 | )) | |
| 68 | ) | |
| 69 | : null} | |
| 70 | </> | |
| 71 | ); | |
| 72 | } |
framework/meta/nextjs/generate/basic.tsx created+171| ... | ... | @@ -0,0 +1,171 @@ |
| 1 | import type { ResolvedMetadata } from "../types/metadata-interface"; | |
| 2 | ||
| 3 | import React from "react"; | |
| 4 | import { Meta, MultiMeta } from "./meta"; | |
| 5 | ||
| 6 | export function BasicMetadata({ metadata }: { metadata: ResolvedMetadata }) { | |
| 7 | return ( | |
| 8 | <> | |
| 9 | <meta charSet="utf-8" /> | |
| 10 | {metadata.title !== null && metadata.title.absolute | |
| 11 | ? <title>{metadata.title.absolute}</title> | |
| 12 | : null} | |
| 13 | <Meta name="description" content={metadata.description} /> | |
| 14 | <Meta name="application-name" content={metadata.applicationName} /> | |
| 15 | {metadata.authors | |
| 16 | ? metadata.authors.map((author, index) => ( | |
| 17 | <React.Fragment key={index}> | |
| 18 | {author.url && <link rel="author" href={author.url.toString()} />} | |
| 19 | <Meta name="author" content={author.name} /> | |
| 20 | </React.Fragment> | |
| 21 | )) | |
| 22 | : null} | |
| 23 | {metadata.manifest | |
| 24 | ? <link rel="manifest" href={metadata.manifest.toString()} /> | |
| 25 | : null} | |
| 26 | <Meta name="generator" content={metadata.generator} /> | |
| 27 | <Meta name="keywords" content={metadata.keywords?.join(",")} /> | |
| 28 | <Meta name="referrer" content={metadata.referrer} /> | |
| 29 | {metadata.themeColor | |
| 30 | ? metadata.themeColor.map((themeColor, index) => ( | |
| 31 | <Meta | |
| 32 | key={index} | |
| 33 | name="theme-color" | |
| 34 | content={themeColor.color} | |
| 35 | media={themeColor.media} | |
| 36 | /> | |
| 37 | )) | |
| 38 | : null} | |
| 39 | <Meta name="color-scheme" content={metadata.colorScheme} /> | |
| 40 | <Meta name="viewport" content={metadata.viewport} /> | |
| 41 | <Meta name="creator" content={metadata.creator} /> | |
| 42 | <Meta name="publisher" content={metadata.publisher} /> | |
| 43 | <Meta name="robots" content={metadata.robots?.basic} /> | |
| 44 | <Meta name="googlebot" content={metadata.robots?.googleBot} /> | |
| 45 | <Meta name="abstract" content={metadata.abstract} /> | |
| 46 | {metadata.archives | |
| 47 | ? metadata.archives.map((archive) => ( | |
| 48 | <link rel="archives" href={archive} key={archive} /> | |
| 49 | )) | |
| 50 | : null} | |
| 51 | {metadata.assets | |
| 52 | ? metadata.assets.map((asset) => ( | |
| 53 | <link rel="assets" href={asset} key={asset} /> | |
| 54 | )) | |
| 55 | : null} | |
| 56 | {metadata.bookmarks | |
| 57 | ? metadata.bookmarks.map((bookmark) => ( | |
| 58 | <link rel="bookmarks" href={bookmark} key={bookmark} /> | |
| 59 | )) | |
| 60 | : null} | |
| 61 | <Meta name="category" content={metadata.category} /> | |
| 62 | <Meta name="classification" content={metadata.classification} /> | |
| 63 | {metadata.other | |
| 64 | ? Object.entries(metadata.other).map(([name, content]) => ( | |
| 65 | <Meta | |
| 66 | key={name} | |
| 67 | name={name} | |
| 68 | content={Array.isArray(content) ? content.join(",") : content} | |
| 69 | /> | |
| 70 | )) | |
| 71 | : null} | |
| 72 | </> | |
| 73 | ); | |
| 74 | } | |
| 75 | ||
| 76 | export function ItunesMeta({ itunes }: { itunes: ResolvedMetadata["itunes"] }) { | |
| 77 | if (!itunes) return null; | |
| 78 | const { appId, appArgument } = itunes; | |
| 79 | let content = `app-id=${appId}`; | |
| 80 | if (appArgument) { | |
| 81 | content += `, app-argument=${appArgument}`; | |
| 82 | } | |
| 83 | return <meta name="apple-itunes-app" content={content} />; | |
| 84 | } | |
| 85 | ||
| 86 | const formatDetectionKeys = [ | |
| 87 | "telephone", | |
| 88 | "date", | |
| 89 | "address", | |
| 90 | "email", | |
| 91 | "url", | |
| 92 | ] as const; | |
| 93 | export function FormatDetectionMeta({ | |
| 94 | formatDetection, | |
| 95 | }: { | |
| 96 | formatDetection: ResolvedMetadata["formatDetection"]; | |
| 97 | }) { | |
| 98 | if (!formatDetection) return null; | |
| 99 | let content = ""; | |
| 100 | for (const key of formatDetectionKeys) { | |
| 101 | if (key in formatDetection) { | |
| 102 | if (content) content += ", "; | |
| 103 | content += `${key}=no`; | |
| 104 | } | |
| 105 | } | |
| 106 | return <meta name="format-detection" content={content} />; | |
| 107 | } | |
| 108 | ||
| 109 | export function AppleWebAppMeta({ | |
| 110 | appleWebApp, | |
| 111 | }: { | |
| 112 | appleWebApp: ResolvedMetadata["appleWebApp"]; | |
| 113 | }) { | |
| 114 | if (!appleWebApp) return null; | |
| 115 | const { capable, title, startupImage, statusBarStyle } = appleWebApp; | |
| 116 | ||
| 117 | return ( | |
| 118 | <> | |
| 119 | {capable | |
| 120 | ? <meta name="apple-mobile-web-app-capable" content="yes" /> | |
| 121 | : null} | |
| 122 | <Meta name="apple-mobile-web-app-title" content={title} /> | |
| 123 | {startupImage | |
| 124 | ? startupImage.map((image, index) => ( | |
| 125 | <link | |
| 126 | key={index} | |
| 127 | href={image.url} | |
| 128 | media={image.media} | |
| 129 | rel="apple-touch-startup-image" | |
| 130 | /> | |
| 131 | )) | |
| 132 | : null} | |
| 133 | {statusBarStyle | |
| 134 | ? ( | |
| 135 | <meta | |
| 136 | name="apple-mobile-web-app-status-bar-style" | |
| 137 | content={statusBarStyle} | |
| 138 | /> | |
| 139 | ) | |
| 140 | : null} | |
| 141 | </> | |
| 142 | ); | |
| 143 | } | |
| 144 | ||
| 145 | export function VerificationMeta({ | |
| 146 | verification, | |
| 147 | }: { | |
| 148 | verification: ResolvedMetadata["verification"]; | |
| 149 | }) { | |
| 150 | if (!verification) return null; | |
| 151 | ||
| 152 | return ( | |
| 153 | <> | |
| 154 | <MultiMeta | |
| 155 | namePrefix="google-site-verification" | |
| 156 | contents={verification.google} | |
| 157 | /> | |
| 158 | <MultiMeta namePrefix="y_key" contents={verification.yahoo} /> | |
| 159 | <MultiMeta | |
| 160 | namePrefix="yandex-verification" | |
| 161 | contents={verification.yandex} | |
| 162 | /> | |
| 163 | <MultiMeta namePrefix="me" contents={verification.me} /> | |
| 164 | {verification.other | |
| 165 | ? Object.entries(verification.other).map(([key, value], index) => ( | |
| 166 | <MultiMeta key={key + index} namePrefix={key} contents={value} /> | |
| 167 | )) | |
| 168 | : null} | |
| 169 | </> | |
| 170 | ); | |
| 171 | } |
framework/meta/nextjs/generate/icons.tsx created+62| ... | ... | @@ -0,0 +1,62 @@ |
| 1 | import type { ResolvedMetadata } from "../types/metadata-interface"; | |
| 2 | import type { Icon, IconDescriptor } from "../types/metadata-types"; | |
| 3 | ||
| 4 | import React from "react"; | |
| 5 | ||
| 6 | function IconDescriptorLink({ icon }: { icon: IconDescriptor }) { | |
| 7 | const { url, rel = "icon", ...props } = icon; | |
| 8 | ||
| 9 | return <link rel={rel} href={url.toString()} {...props} />; | |
| 10 | } | |
| 11 | ||
| 12 | function IconLink({ rel, icon }: { rel?: string; icon: Icon }) { | |
| 13 | if (typeof icon === "object" && !(icon instanceof URL)) { | |
| 14 | if (rel) icon.rel = rel; | |
| 15 | return <IconDescriptorLink icon={icon} />; | |
| 16 | } else { | |
| 17 | const href = icon.toString(); | |
| 18 | return <link rel={rel} href={href} />; | |
| 19 | } | |
| 20 | } | |
| 21 | ||
| 22 | export function IconsMetadata({ icons }: { icons: ResolvedMetadata["icons"] }) { | |
| 23 | if (!icons) return null; | |
| 24 | ||
| 25 | const shortcutList = icons.shortcut; | |
| 26 | const iconList = icons.icon; | |
| 27 | const appleList = icons.apple; | |
| 28 | const otherList = icons.other; | |
| 29 | ||
| 30 | return ( | |
| 31 | <> | |
| 32 | {shortcutList | |
| 33 | ? shortcutList.map((icon, index) => ( | |
| 34 | <IconLink | |
| 35 | key={`shortcut-${index}`} | |
| 36 | rel="shortcut icon" | |
| 37 | icon={icon} | |
| 38 | /> | |
| 39 | )) | |
| 40 | : null} | |
| 41 | {iconList | |
| 42 | ? iconList.map((icon, index) => ( | |
| 43 | <IconLink key={`shortcut-${index}`} rel="icon" icon={icon} /> | |
| 44 | )) | |
| 45 | : null} | |
| 46 | {appleList | |
| 47 | ? appleList.map((icon, index) => ( | |
| 48 | <IconLink | |
| 49 | key={`apple-${index}`} | |
| 50 | rel="apple-touch-icon" | |
| 51 | icon={icon} | |
| 52 | /> | |
| 53 | )) | |
| 54 | : null} | |
| 55 | {otherList | |
| 56 | ? otherList.map((icon, index) => ( | |
| 57 | <IconDescriptorLink key={`other-${index}`} icon={icon} /> | |
| 58 | )) | |
| 59 | : null} | |
| 60 | </> | |
| 61 | ); | |
| 62 | } |
framework/meta/nextjs/generate/meta.tsx created+124| ... | ... | @@ -0,0 +1,124 @@ |
| 1 | import React from "react"; | |
| 2 | ||
| 3 | export function Meta({ | |
| 4 | name, | |
| 5 | property, | |
| 6 | content, | |
| 7 | media, | |
| 8 | }: { | |
| 9 | name?: string; | |
| 10 | property?: string; | |
| 11 | media?: string; | |
| 12 | content: string | number | URL | null | undefined; | |
| 13 | }): React.ReactElement | null { | |
| 14 | if (typeof content !== "undefined" && content !== null && content !== "") { | |
| 15 | return ( | |
| 16 | <meta | |
| 17 | {...(name ? { name } : { property })} | |
| 18 | {...(media ? { media } : undefined)} | |
| 19 | content={typeof content === "string" ? content : content.toString()} | |
| 20 | /> | |
| 21 | ); | |
| 22 | } | |
| 23 | return null; | |
| 24 | } | |
| 25 | ||
| 26 | type ExtendMetaContent = Record< | |
| 27 | string, | |
| 28 | undefined | string | URL | number | boolean | null | undefined | |
| 29 | >; | |
| 30 | type MultiMetaContent = | |
| 31 | | (ExtendMetaContent | string | URL | number)[] | |
| 32 | | null | |
| 33 | | undefined; | |
| 34 | ||
| 35 | function camelToSnake(camelCaseStr: string) { | |
| 36 | return camelCaseStr.replace(/([A-Z])/g, function (match) { | |
| 37 | return "_" + match.toLowerCase(); | |
| 38 | }); | |
| 39 | } | |
| 40 | ||
| 41 | function getMetaKey(prefix: string, key: string) { | |
| 42 | // Use `twitter:image` and `og:image` instead of `twitter:image:url` and `og:image:url` | |
| 43 | // to be more compatible as it's a more common format | |
| 44 | if ((prefix === "og:image" || prefix === "twitter:image") && key === "url") { | |
| 45 | return prefix; | |
| 46 | } | |
| 47 | if (prefix.startsWith("og:") || prefix.startsWith("twitter:")) { | |
| 48 | key = camelToSnake(key); | |
| 49 | } | |
| 50 | return prefix + ":" + key; | |
| 51 | } | |
| 52 | ||
| 53 | function ExtendMeta({ | |
| 54 | content, | |
| 55 | namePrefix, | |
| 56 | propertyPrefix, | |
| 57 | }: { | |
| 58 | content?: ExtendMetaContent; | |
| 59 | namePrefix?: string; | |
| 60 | propertyPrefix?: string; | |
| 61 | }) { | |
| 62 | const keyPrefix = namePrefix || propertyPrefix; | |
| 63 | if (!content) return null; | |
| 64 | return ( | |
| 65 | <React.Fragment> | |
| 66 | {Object.entries(content).map(([k, v], index) => { | |
| 67 | return typeof v === "undefined" ? null : ( | |
| 68 | <Meta | |
| 69 | key={keyPrefix + ":" + k + "_" + index} | |
| 70 | {...(propertyPrefix && { property: getMetaKey(propertyPrefix, k) })} | |
| 71 | {...(namePrefix && { name: getMetaKey(namePrefix, k) })} | |
| 72 | content={typeof v === "string" ? v : v?.toString()} | |
| 73 | /> | |
| 74 | ); | |
| 75 | })} | |
| 76 | </React.Fragment> | |
| 77 | ); | |
| 78 | } | |
| 79 | ||
| 80 | export function MultiMeta({ | |
| 81 | propertyPrefix, | |
| 82 | namePrefix, | |
| 83 | contents, | |
| 84 | }: { | |
| 85 | propertyPrefix?: string; | |
| 86 | namePrefix?: string; | |
| 87 | contents?: MultiMetaContent | null; | |
| 88 | }) { | |
| 89 | if (typeof contents === "undefined" || contents === null) { | |
| 90 | return null; | |
| 91 | } | |
| 92 | ||
| 93 | const keyPrefix = propertyPrefix || namePrefix; | |
| 94 | return ( | |
| 95 | <> | |
| 96 | {contents.map((content, index) => { | |
| 97 | if ( | |
| 98 | typeof content === "string" || | |
| 99 | typeof content === "number" || | |
| 100 | content instanceof URL | |
| 101 | ) { | |
| 102 | return ( | |
| 103 | <Meta | |
| 104 | key={keyPrefix + "_" + index} | |
| 105 | {...(propertyPrefix | |
| 106 | ? { property: propertyPrefix } | |
| 107 | : { name: namePrefix })} | |
| 108 | content={content} | |
| 109 | /> | |
| 110 | ); | |
| 111 | } else { | |
| 112 | return ( | |
| 113 | <ExtendMeta | |
| 114 | key={keyPrefix + "_" + index} | |
| 115 | namePrefix={namePrefix} | |
| 116 | propertyPrefix={propertyPrefix} | |
| 117 | content={content} | |
| 118 | /> | |
| 119 | ); | |
| 120 | } | |
| 121 | })} | |
| 122 | </> | |
| 123 | ); | |
| 124 | } |
framework/meta/nextjs/generate/opengraph.tsx created+316| ... | ... | @@ -0,0 +1,316 @@ |
| 1 | import type { ResolvedMetadata } from "../types/metadata-interface"; | |
| 2 | import type { TwitterAppDescriptor } from "../types/twitter-types"; | |
| 3 | ||
| 4 | import React from "react"; | |
| 5 | import { Meta, MultiMeta } from "./meta"; | |
| 6 | ||
| 7 | export function OpenGraphMetadata({ | |
| 8 | openGraph, | |
| 9 | }: { | |
| 10 | openGraph: ResolvedMetadata["openGraph"]; | |
| 11 | }) { | |
| 12 | if (!openGraph) { | |
| 13 | return null; | |
| 14 | } | |
| 15 | ||
| 16 | let typedOpenGraph; | |
| 17 | if ("type" in openGraph) { | |
| 18 | switch (openGraph.type) { | |
| 19 | case "website": | |
| 20 | typedOpenGraph = <Meta property="og:type" content="website" />; | |
| 21 | break; | |
| 22 | case "article": | |
| 23 | typedOpenGraph = ( | |
| 24 | <> | |
| 25 | <Meta property="og:type" content="article" /> | |
| 26 | <Meta | |
| 27 | property="article:published_time" | |
| 28 | content={openGraph.publishedTime?.toString()} | |
| 29 | /> | |
| 30 | <Meta | |
| 31 | property="article:modified_time" | |
| 32 | content={openGraph.modifiedTime?.toString()} | |
| 33 | /> | |
| 34 | <Meta | |
| 35 | property="article:expiration_time" | |
| 36 | content={openGraph.expirationTime?.toString()} | |
| 37 | /> | |
| 38 | <MultiMeta | |
| 39 | propertyPrefix="article:author" | |
| 40 | contents={openGraph.authors} | |
| 41 | /> | |
| 42 | <Meta property="article:section" content={openGraph.section} /> | |
| 43 | <MultiMeta propertyPrefix="article:tag" contents={openGraph.tags} /> | |
| 44 | </> | |
| 45 | ); | |
| 46 | break; | |
| 47 | case "book": | |
| 48 | typedOpenGraph = ( | |
| 49 | <> | |
| 50 | <Meta property="og:type" content="book" /> | |
| 51 | <Meta property="book:isbn" content={openGraph.isbn} /> | |
| 52 | <Meta | |
| 53 | property="book:release_date" | |
| 54 | content={openGraph.releaseDate} | |
| 55 | /> | |
| 56 | <MultiMeta | |
| 57 | propertyPrefix="book:author" | |
| 58 | contents={openGraph.authors} | |
| 59 | /> | |
| 60 | <MultiMeta propertyPrefix="book:tag" contents={openGraph.tags} /> | |
| 61 | </> | |
| 62 | ); | |
| 63 | break; | |
| 64 | case "profile": | |
| 65 | typedOpenGraph = ( | |
| 66 | <> | |
| 67 | <Meta property="og:type" content="profile" /> | |
| 68 | <Meta property="profile:first_name" content={openGraph.firstName} /> | |
| 69 | <Meta property="profile:last_name" content={openGraph.lastName} /> | |
| 70 | <Meta property="profile:username" content={openGraph.username} /> | |
| 71 | <Meta property="profile:gender" content={openGraph.gender} /> | |
| 72 | </> | |
| 73 | ); | |
| 74 | break; | |
| 75 | case "music.song": | |
| 76 | typedOpenGraph = ( | |
| 77 | <> | |
| 78 | <Meta property="og:type" content="music.song" /> | |
| 79 | <Meta | |
| 80 | property="music:duration" | |
| 81 | content={openGraph.duration?.toString()} | |
| 82 | /> | |
| 83 | <MultiMeta | |
| 84 | propertyPrefix="music:album" | |
| 85 | contents={openGraph.albums} | |
| 86 | /> | |
| 87 | <MultiMeta | |
| 88 | propertyPrefix="music:musician" | |
| 89 | contents={openGraph.musicians} | |
| 90 | /> | |
| 91 | </> | |
| 92 | ); | |
| 93 | break; | |
| 94 | case "music.album": | |
| 95 | typedOpenGraph = ( | |
| 96 | <> | |
| 97 | <Meta property="og:type" content="music.album" /> | |
| 98 | <MultiMeta propertyPrefix="music:song" contents={openGraph.songs} /> | |
| 99 | <MultiMeta | |
| 100 | propertyPrefix="music:musician" | |
| 101 | contents={openGraph.musicians} | |
| 102 | /> | |
| 103 | <Meta | |
| 104 | property="music:release_date" | |
| 105 | content={openGraph.releaseDate} | |
| 106 | /> | |
| 107 | </> | |
| 108 | ); | |
| 109 | break; | |
| 110 | case "music.playlist": | |
| 111 | typedOpenGraph = ( | |
| 112 | <> | |
| 113 | <Meta property="og:type" content="music.playlist" /> | |
| 114 | <MultiMeta propertyPrefix="music:song" contents={openGraph.songs} /> | |
| 115 | <MultiMeta | |
| 116 | propertyPrefix="music:creator" | |
| 117 | contents={openGraph.creators} | |
| 118 | /> | |
| 119 | </> | |
| 120 | ); | |
| 121 | break; | |
| 122 | case "music.radio_station": | |
| 123 | typedOpenGraph = ( | |
| 124 | <> | |
| 125 | <Meta property="og:type" content="music.radio_station" /> | |
| 126 | <MultiMeta | |
| 127 | propertyPrefix="music:creator" | |
| 128 | contents={openGraph.creators} | |
| 129 | /> | |
| 130 | </> | |
| 131 | ); | |
| 132 | break; | |
| 133 | case "video.movie": | |
| 134 | typedOpenGraph = ( | |
| 135 | <> | |
| 136 | <Meta property="og:type" content="video.movie" /> | |
| 137 | <MultiMeta | |
| 138 | propertyPrefix="video:actor" | |
| 139 | contents={openGraph.actors} | |
| 140 | /> | |
| 141 | <MultiMeta | |
| 142 | propertyPrefix="video:director" | |
| 143 | contents={openGraph.directors} | |
| 144 | /> | |
| 145 | <MultiMeta | |
| 146 | propertyPrefix="video:writer" | |
| 147 | contents={openGraph.writers} | |
| 148 | /> | |
| 149 | <Meta property="video:duration" content={openGraph.duration} /> | |
| 150 | <Meta | |
| 151 | property="video:release_date" | |
| 152 | content={openGraph.releaseDate} | |
| 153 | /> | |
| 154 | <MultiMeta propertyPrefix="video:tag" contents={openGraph.tags} /> | |
| 155 | </> | |
| 156 | ); | |
| 157 | break; | |
| 158 | case "video.episode": | |
| 159 | typedOpenGraph = ( | |
| 160 | <> | |
| 161 | <Meta property="og:type" content="video.episode" /> | |
| 162 | <MultiMeta | |
| 163 | propertyPrefix="video:actor" | |
| 164 | contents={openGraph.actors} | |
| 165 | /> | |
| 166 | <MultiMeta | |
| 167 | propertyPrefix="video:director" | |
| 168 | contents={openGraph.directors} | |
| 169 | /> | |
| 170 | <MultiMeta | |
| 171 | propertyPrefix="video:writer" | |
| 172 | contents={openGraph.writers} | |
| 173 | /> | |
| 174 | <Meta property="video:duration" content={openGraph.duration} /> | |
| 175 | <Meta | |
| 176 | property="video:release_date" | |
| 177 | content={openGraph.releaseDate} | |
| 178 | /> | |
| 179 | <MultiMeta propertyPrefix="video:tag" contents={openGraph.tags} /> | |
| 180 | <Meta property="video:series" content={openGraph.series} /> | |
| 181 | </> | |
| 182 | ); | |
| 183 | break; | |
| 184 | case "video.tv_show": | |
| 185 | typedOpenGraph = <Meta property="og:type" content="video.tv_show" />; | |
| 186 | break; | |
| 187 | case "video.other": | |
| 188 | typedOpenGraph = <Meta property="og:type" content="video.other" />; | |
| 189 | break; | |
| 190 | default: | |
| 191 | throw new Error("Invalid OpenGraph type: " + (openGraph as any).type); | |
| 192 | } | |
| 193 | } | |
| 194 | ||
| 195 | return ( | |
| 196 | <> | |
| 197 | <Meta property="og:determiner" content={openGraph.determiner} /> | |
| 198 | <Meta property="og:title" content={openGraph.title?.absolute} /> | |
| 199 | <Meta property="og:description" content={openGraph.description} /> | |
| 200 | <Meta property="og:url" content={openGraph.url?.toString()} /> | |
| 201 | <Meta property="og:site_name" content={openGraph.siteName} /> | |
| 202 | <Meta property="og:locale" content={openGraph.locale} /> | |
| 203 | <Meta property="og:country_name" content={openGraph.countryName} /> | |
| 204 | <Meta property="og:ttl" content={openGraph.ttl?.toString()} /> | |
| 205 | <MultiMeta propertyPrefix="og:image" contents={openGraph.images} /> | |
| 206 | <MultiMeta propertyPrefix="og:video" contents={openGraph.videos} /> | |
| 207 | <MultiMeta propertyPrefix="og:audio" contents={openGraph.audio} /> | |
| 208 | <MultiMeta propertyPrefix="og:email" contents={openGraph.emails} /> | |
| 209 | <MultiMeta | |
| 210 | propertyPrefix="og:phone_number" | |
| 211 | contents={openGraph.phoneNumbers} | |
| 212 | /> | |
| 213 | <MultiMeta | |
| 214 | propertyPrefix="og:fax_number" | |
| 215 | contents={openGraph.faxNumbers} | |
| 216 | /> | |
| 217 | <MultiMeta | |
| 218 | propertyPrefix="og:locale:alternate" | |
| 219 | contents={openGraph.alternateLocale} | |
| 220 | /> | |
| 221 | {typedOpenGraph} | |
| 222 | </> | |
| 223 | ); | |
| 224 | } | |
| 225 | ||
| 226 | function TwitterAppItem({ | |
| 227 | app, | |
| 228 | type, | |
| 229 | }: { | |
| 230 | app: TwitterAppDescriptor; | |
| 231 | type: "iphone" | "ipad" | "googleplay"; | |
| 232 | }) { | |
| 233 | return ( | |
| 234 | <> | |
| 235 | <Meta name={`twitter:app:name:${type}`} content={app.name} /> | |
| 236 | <Meta name={`twitter:app:id:${type}`} content={app.id[type]} /> | |
| 237 | <Meta | |
| 238 | name={`twitter:app:url:${type}`} | |
| 239 | content={app.url?.[type]?.toString()} | |
| 240 | /> | |
| 241 | </> | |
| 242 | ); | |
| 243 | } | |
| 244 | ||
| 245 | export function TwitterMetadata({ | |
| 246 | twitter, | |
| 247 | }: { | |
| 248 | twitter: ResolvedMetadata["twitter"]; | |
| 249 | }) { | |
| 250 | if (!twitter) return null; | |
| 251 | const { card } = twitter; | |
| 252 | ||
| 253 | return ( | |
| 254 | <> | |
| 255 | <Meta name="twitter:card" content={card} /> | |
| 256 | <Meta name="twitter:site" content={twitter.site} /> | |
| 257 | <Meta name="twitter:site:id" content={twitter.siteId} /> | |
| 258 | <Meta name="twitter:creator" content={twitter.creator} /> | |
| 259 | <Meta name="twitter:creator:id" content={twitter.creatorId} /> | |
| 260 | <Meta name="twitter:title" content={twitter.title?.absolute} /> | |
| 261 | <Meta name="twitter:description" content={twitter.description} /> | |
| 262 | <MultiMeta namePrefix="twitter:image" contents={twitter.images} /> | |
| 263 | {card === "player" | |
| 264 | ? twitter.players.map((player, index) => ( | |
| 265 | <React.Fragment key={index}> | |
| 266 | <Meta | |
| 267 | name="twitter:player" | |
| 268 | content={player.playerUrl.toString()} | |
| 269 | /> | |
| 270 | <Meta | |
| 271 | name="twitter:player:stream" | |
| 272 | content={player.streamUrl.toString()} | |
| 273 | /> | |
| 274 | <Meta name="twitter:player:width" content={player.width} /> | |
| 275 | <Meta name="twitter:player:height" content={player.height} /> | |
| 276 | </React.Fragment> | |
| 277 | )) | |
| 278 | : null} | |
| 279 | {card === "app" | |
| 280 | ? ( | |
| 281 | <> | |
| 282 | <TwitterAppItem app={twitter.app} type="iphone" /> | |
| 283 | <TwitterAppItem app={twitter.app} type="ipad" /> | |
| 284 | <TwitterAppItem app={twitter.app} type="googleplay" /> | |
| 285 | </> | |
| 286 | ) | |
| 287 | : null} | |
| 288 | </> | |
| 289 | ); | |
| 290 | } | |
| 291 | ||
| 292 | export function AppLinksMeta({ | |
| 293 | appLinks, | |
| 294 | }: { | |
| 295 | appLinks: ResolvedMetadata["appLinks"]; | |
| 296 | }) { | |
| 297 | if (!appLinks) return null; | |
| 298 | return ( | |
| 299 | <> | |
| 300 | <MultiMeta propertyPrefix="al:ios" contents={appLinks.ios} /> | |
| 301 | <MultiMeta propertyPrefix="al:iphone" contents={appLinks.iphone} /> | |
| 302 | <MultiMeta propertyPrefix="al:ipad" contents={appLinks.ipad} /> | |
| 303 | <MultiMeta propertyPrefix="al:android" contents={appLinks.android} /> | |
| 304 | <MultiMeta | |
| 305 | propertyPrefix="al:windows_phone" | |
| 306 | contents={appLinks.windows_phone} | |
| 307 | /> | |
| 308 | <MultiMeta propertyPrefix="al:windows" contents={appLinks.windows} /> | |
| 309 | <MultiMeta | |
| 310 | propertyPrefix="al:windows_universal" | |
| 311 | contents={appLinks.windows_universal} | |
| 312 | /> | |
| 313 | <MultiMeta propertyPrefix="al:web" contents={appLinks.web} /> | |
| 314 | </> | |
| 315 | ); | |
| 316 | } |
framework/meta/nextjs/generate/utils.ts created+20| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | function resolveArray<T>(value: T): T[] { | |
| 2 | if (Array.isArray(value)) { | |
| 3 | return value; | |
| 4 | } | |
| 5 | return [value]; | |
| 6 | } | |
| 7 | ||
| 8 | function resolveAsArrayOrUndefined<T extends unknown | readonly unknown[]>( | |
| 9 | value: T | T[] | undefined | null, | |
| 10 | ): undefined | T[] { | |
| 11 | if (typeof value === "undefined" || value === null) { | |
| 12 | return undefined; | |
| 13 | } | |
| 14 | if (Array.isArray(value)) { | |
| 15 | return value; | |
| 16 | } | |
| 17 | return [value]; | |
| 18 | } | |
| 19 | ||
| 20 | export { resolveArray, resolveAsArrayOrUndefined }; |
framework/meta/nextjs/get-metadata-route.ts created+67| ... | ... | @@ -0,0 +1,67 @@ |
| 1 | import { isMetadataRoute, isMetadataRouteFile } from "./is-metadata-route"; | |
| 2 | import path from "../../shared/lib/isomorphic/path"; | |
| 3 | import { djb2Hash } from "../../shared/lib/hash"; | |
| 4 | ||
| 5 | /* | |
| 6 | * If there's special convention like (...) or @ in the page path, | |
| 7 | * Give it a unique hash suffix to avoid conflicts | |
| 8 | * | |
| 9 | * e.g. | |
| 10 | * /app/open-graph.tsx -> /open-graph/route | |
| 11 | * /app/(post)/open-graph.tsx -> /open-graph/route-[0-9a-z]{6} | |
| 12 | */ | |
| 13 | export function getMetadataRouteSuffix(page: string) { | |
| 14 | let suffix = ""; | |
| 15 | ||
| 16 | if ((page.includes("(") && page.includes(")")) || page.includes("@")) { | |
| 17 | suffix = djb2Hash(page).toString(36).slice(0, 6); | |
| 18 | } | |
| 19 | return suffix; | |
| 20 | } | |
| 21 | ||
| 22 | /** | |
| 23 | * Map metadata page key to the corresponding route | |
| 24 | * | |
| 25 | * static file page key: /app/robots.txt -> /robots.xml -> /robots.txt/route | |
| 26 | * dynamic route page key: /app/robots.tsx -> /robots -> /robots.txt/route | |
| 27 | * | |
| 28 | * @param page | |
| 29 | * @returns | |
| 30 | */ | |
| 31 | export function normalizeMetadataRoute(page: string) { | |
| 32 | let route = page; | |
| 33 | if (isMetadataRoute(page)) { | |
| 34 | // Remove the file extension, e.g. /route-path/robots.txt -> /route-path | |
| 35 | const pathnamePrefix = page.slice(0, -(path.basename(page).length + 1)); | |
| 36 | const suffix = getMetadataRouteSuffix(pathnamePrefix); | |
| 37 | ||
| 38 | if (route === "/sitemap") { | |
| 39 | route += ".xml"; | |
| 40 | } | |
| 41 | if (route === "/robots") { | |
| 42 | route += ".txt"; | |
| 43 | } | |
| 44 | if (route === "/manifest") { | |
| 45 | route += ".webmanifest"; | |
| 46 | } | |
| 47 | // Support both /<metadata-route.ext> and custom routes /<metadata-route>/route.ts. | |
| 48 | // If it's a metadata file route, we need to append /[id]/route to the page. | |
| 49 | if (!route.endsWith("/route")) { | |
| 50 | const isStaticMetadataFile = isMetadataRouteFile(route, [], true); | |
| 51 | const { dir, name: baseName, ext } = path.parse(route); | |
| 52 | ||
| 53 | const isSingleRoute = page.startsWith("/sitemap") || | |
| 54 | page.startsWith("/robots") || | |
| 55 | page.startsWith("/manifest") || | |
| 56 | isStaticMetadataFile; | |
| 57 | ||
| 58 | route = path.join( | |
| 59 | dir, | |
| 60 | `${baseName}${suffix ? `-${suffix}` : ""}${ext}`, | |
| 61 | isSingleRoute ? "" : "[[...__metadata_id__]]", | |
| 62 | "route", | |
| 63 | ); | |
| 64 | } | |
| 65 | } | |
| 66 | return route; | |
| 67 | } |
framework/meta/nextjs/is-metadata-route.ts created+136| ... | ... | @@ -0,0 +1,136 @@ |
| 1 | export const STATIC_METADATA_IMAGES = { | |
| 2 | icon: { | |
| 3 | filename: "icon", | |
| 4 | extensions: ["ico", "jpg", "jpeg", "png", "svg"], | |
| 5 | }, | |
| 6 | apple: { | |
| 7 | filename: "apple-icon", | |
| 8 | extensions: ["jpg", "jpeg", "png"], | |
| 9 | }, | |
| 10 | favicon: { | |
| 11 | filename: "favicon", | |
| 12 | extensions: ["ico"], | |
| 13 | }, | |
| 14 | openGraph: { | |
| 15 | filename: "opengraph-image", | |
| 16 | extensions: ["jpg", "jpeg", "png", "gif"], | |
| 17 | }, | |
| 18 | twitter: { | |
| 19 | filename: "twitter-image", | |
| 20 | extensions: ["jpg", "jpeg", "png", "gif"], | |
| 21 | }, | |
| 22 | } as const; | |
| 23 | ||
| 24 | // Match routes that are metadata routes, e.g. /sitemap.xml, /favicon.<ext>, /<icon>.<ext>, etc. | |
| 25 | // TODO-METADATA: support more metadata routes with more extensions | |
| 26 | const defaultExtensions = ["js", "jsx", "ts", "tsx"]; | |
| 27 | ||
| 28 | const getExtensionRegexString = (extensions: readonly string[]) => | |
| 29 | `(?:${extensions.join("|")})`; | |
| 30 | ||
| 31 | // When you only pass the file extension as `[]`, it will only match the static convention files | |
| 32 | // e.g. /robots.txt, /sitemap.xml, /favicon.ico, /manifest.json | |
| 33 | // When you pass the file extension as `['js', 'jsx', 'ts', 'tsx']`, it will also match the dynamic convention files | |
| 34 | // e.g. /robots.js, /sitemap.tsx, /favicon.jsx, /manifest.ts | |
| 35 | // When `withExtension` is false, it will match the static convention files without the extension, by default it's true | |
| 36 | // e.g. /robots, /sitemap, /favicon, /manifest, use to match dynamic API routes like app/robots.ts | |
| 37 | export function isMetadataRouteFile( | |
| 38 | appDirRelativePath: string, | |
| 39 | pageExtensions: string[], | |
| 40 | withExtension: boolean, | |
| 41 | ) { | |
| 42 | const metadataRouteFilesRegex = [ | |
| 43 | new RegExp( | |
| 44 | `^[\\\\/]robots${ | |
| 45 | withExtension | |
| 46 | ? `\\.${getExtensionRegexString(pageExtensions.concat("txt"))}` | |
| 47 | : "" | |
| 48 | }`, | |
| 49 | ), | |
| 50 | new RegExp( | |
| 51 | `^[\\\\/]sitemap${ | |
| 52 | withExtension | |
| 53 | ? `\\.${getExtensionRegexString(pageExtensions.concat("xml"))}` | |
| 54 | : "" | |
| 55 | }`, | |
| 56 | ), | |
| 57 | new RegExp( | |
| 58 | `^[\\\\/]manifest${ | |
| 59 | withExtension | |
| 60 | ? `\\.${ | |
| 61 | getExtensionRegexString( | |
| 62 | pageExtensions.concat("webmanifest", "json"), | |
| 63 | ) | |
| 64 | }` | |
| 65 | : "" | |
| 66 | }`, | |
| 67 | ), | |
| 68 | new RegExp(`^[\\\\/]favicon\\.ico$`), | |
| 69 | // TODO-METADATA: add dynamic routes for metadata images | |
| 70 | new RegExp( | |
| 71 | `[\\\\/]${STATIC_METADATA_IMAGES.icon.filename}${ | |
| 72 | withExtension | |
| 73 | ? `\\.${ | |
| 74 | getExtensionRegexString( | |
| 75 | pageExtensions.concat(STATIC_METADATA_IMAGES.icon.extensions), | |
| 76 | ) | |
| 77 | }` | |
| 78 | : "" | |
| 79 | }`, | |
| 80 | ), | |
| 81 | new RegExp( | |
| 82 | `[\\\\/]${STATIC_METADATA_IMAGES.apple.filename}${ | |
| 83 | withExtension | |
| 84 | ? `\\.${ | |
| 85 | getExtensionRegexString( | |
| 86 | pageExtensions.concat(STATIC_METADATA_IMAGES.apple.extensions), | |
| 87 | ) | |
| 88 | }` | |
| 89 | : "" | |
| 90 | }`, | |
| 91 | ), | |
| 92 | new RegExp( | |
| 93 | `[\\\\/]${STATIC_METADATA_IMAGES.openGraph.filename}${ | |
| 94 | withExtension | |
| 95 | ? `\\.${ | |
| 96 | getExtensionRegexString( | |
| 97 | pageExtensions.concat( | |
| 98 | STATIC_METADATA_IMAGES.openGraph.extensions, | |
| 99 | ), | |
| 100 | ) | |
| 101 | }` | |
| 102 | : "" | |
| 103 | }`, | |
| 104 | ), | |
| 105 | new RegExp( | |
| 106 | `[\\\\/]${STATIC_METADATA_IMAGES.twitter.filename}${ | |
| 107 | withExtension | |
| 108 | ? `\\.${ | |
| 109 | getExtensionRegexString( | |
| 110 | pageExtensions.concat(STATIC_METADATA_IMAGES.twitter.extensions), | |
| 111 | ) | |
| 112 | }` | |
| 113 | : "" | |
| 114 | }`, | |
| 115 | ), | |
| 116 | ]; | |
| 117 | ||
| 118 | return metadataRouteFilesRegex.some((r) => r.test(appDirRelativePath)); | |
| 119 | } | |
| 120 | ||
| 121 | /* | |
| 122 | * Remove the 'app' prefix or '/route' suffix, only check the route name since they're only allowed in root app directory | |
| 123 | * e.g. | |
| 124 | * /app/robots -> /robots | |
| 125 | * app/robots -> /robots | |
| 126 | * /robots -> /robots | |
| 127 | */ | |
| 128 | export function isMetadataRoute(route: string): boolean { | |
| 129 | let page = route.replace(/^\/?app\//, "").replace(/\/route$/, ""); | |
| 130 | if (page[0] !== "/") page = "/" + page; | |
| 131 | ||
| 132 | return ( | |
| 133 | !page.endsWith("/page") && | |
| 134 | isMetadataRouteFile(page, defaultExtensions, false) | |
| 135 | ); | |
| 136 | } |
framework/meta/nextjs/metadata.tsx created+58| ... | ... | @@ -0,0 +1,58 @@ |
| 1 | import React from "react"; | |
| 2 | import { | |
| 3 | AppleWebAppMeta, | |
| 4 | BasicMetadata, | |
| 5 | FormatDetectionMeta, | |
| 6 | ItunesMeta, | |
| 7 | VerificationMeta, | |
| 8 | } from "./generate/basic"; | |
| 9 | import { AlternatesMetadata } from "./generate/alternate"; | |
| 10 | import { | |
| 11 | AppLinksMeta, | |
| 12 | OpenGraphMetadata, | |
| 13 | TwitterMetadata, | |
| 14 | } from "./generate/opengraph"; | |
| 15 | import { IconsMetadata } from "./generate/icons"; | |
| 16 | import { accumulateMetadata, resolveMetadata } from "./resolve-metadata"; | |
| 17 | import { LoaderTree } from "../../server/lib/app-dir-module"; | |
| 18 | import { GetDynamicParamFromSegment } from "../../server/app-render/app-render"; | |
| 19 | ||
| 20 | // Generate the actual React elements from the resolved metadata. | |
| 21 | export async function MetadataTree({ | |
| 22 | tree, | |
| 23 | pathname, | |
| 24 | searchParams, | |
| 25 | getDynamicParamFromSegment, | |
| 26 | }: { | |
| 27 | tree: LoaderTree; | |
| 28 | pathname: string; | |
| 29 | searchParams: { [key: string]: any }; | |
| 30 | getDynamicParamFromSegment: GetDynamicParamFromSegment; | |
| 31 | }) { | |
| 32 | const options = { | |
| 33 | pathname, | |
| 34 | }; | |
| 35 | const resolvedMetadata = await resolveMetadata({ | |
| 36 | tree, | |
| 37 | parentParams: {}, | |
| 38 | metadataItems: [], | |
| 39 | searchParams, | |
| 40 | getDynamicParamFromSegment, | |
| 41 | }); | |
| 42 | const metadata = await accumulateMetadata(resolvedMetadata, options); | |
| 43 | ||
| 44 | return ( | |
| 45 | <> | |
| 46 | <BasicMetadata metadata={metadata} /> | |
| 47 | <AlternatesMetadata alternates={metadata.alternates} /> | |
| 48 | <ItunesMeta itunes={metadata.itunes} /> | |
| 49 | <FormatDetectionMeta formatDetection={metadata.formatDetection} /> | |
| 50 | <VerificationMeta verification={metadata.verification} /> | |
| 51 | <AppleWebAppMeta appleWebApp={metadata.appleWebApp} /> | |
| 52 | <OpenGraphMetadata openGraph={metadata.openGraph} /> | |
| 53 | <TwitterMetadata twitter={metadata.twitter} /> | |
| 54 | <AppLinksMeta appLinks={metadata.appLinks} /> | |
| 55 | <IconsMetadata icons={metadata.icons} /> | |
| 56 | </> | |
| 57 | ); | |
| 58 | } |
framework/meta/nextjs/resolve-metadata.ts created+453| ... | ... | @@ -0,0 +1,453 @@ |
| 1 | import type { | |
| 2 | Metadata, | |
| 3 | ResolvedMetadata, | |
| 4 | ResolvingMetadata, | |
| 5 | } from "./types/metadata-interface"; | |
| 6 | import type { MetadataImageModule } from "../../build/webpack/loaders/metadata/types"; | |
| 7 | import type { GetDynamicParamFromSegment } from "../../server/app-render/app-render"; | |
| 8 | import { createDefaultMetadata } from "./default-metadata"; | |
| 9 | import { | |
| 10 | resolveOpenGraph, | |
| 11 | resolveTwitter, | |
| 12 | } from "./resolvers/resolve-opengraph"; | |
| 13 | import { resolveTitle } from "./resolvers/resolve-title"; | |
| 14 | import { resolveAsArrayOrUndefined } from "./generate/utils"; | |
| 15 | import { isClientReference } from "../client-reference"; | |
| 16 | import { | |
| 17 | getLayoutOrPageModule, | |
| 18 | LoaderTree, | |
| 19 | } from "../../server/lib/app-dir-module"; | |
| 20 | import { ComponentsType } from "../../build/webpack/loaders/next-app-loader"; | |
| 21 | import { interopDefault } from "../interop-default"; | |
| 22 | import { | |
| 23 | resolveAlternates, | |
| 24 | resolveAppleWebApp, | |
| 25 | resolveAppLinks, | |
| 26 | resolveRobots, | |
| 27 | resolveThemeColor, | |
| 28 | resolveVerification, | |
| 29 | resolveViewport, | |
| 30 | } from "./resolvers/resolve-basics"; | |
| 31 | import { resolveIcons } from "./resolvers/resolve-icons"; | |
| 32 | import { getTracer } from "../../server/lib/trace/tracer"; | |
| 33 | import { ResolveMetadataSpan } from "../../server/lib/trace/constants"; | |
| 34 | import { Twitter } from "./types/twitter-types"; | |
| 35 | import { OpenGraph } from "./types/opengraph-types"; | |
| 36 | import { PAGE_SEGMENT_KEY } from "../../shared/lib/constants"; | |
| 37 | import process from "node:process"; | |
| 38 | ||
| 39 | type StaticMetadata = Awaited<ReturnType<typeof resolveStaticMetadata>>; | |
| 40 | ||
| 41 | type MetadataResolver = ( | |
| 42 | _parent: ResolvingMetadata, | |
| 43 | ) => Metadata | Promise<Metadata>; | |
| 44 | export type MetadataItems = [ | |
| 45 | Metadata | MetadataResolver | null, | |
| 46 | StaticMetadata, | |
| 47 | ][]; | |
| 48 | ||
| 49 | function mergeStaticMetadata( | |
| 50 | metadata: ResolvedMetadata, | |
| 51 | staticFilesMetadata: StaticMetadata, | |
| 52 | ) { | |
| 53 | if (!staticFilesMetadata) return; | |
| 54 | const { icon, apple, openGraph, twitter, manifest } = staticFilesMetadata; | |
| 55 | if (icon || apple) { | |
| 56 | metadata.icons = { | |
| 57 | icon: icon || [], | |
| 58 | apple: apple || [], | |
| 59 | }; | |
| 60 | } | |
| 61 | if (twitter) { | |
| 62 | const resolvedTwitter = resolveTwitter( | |
| 63 | { ...metadata.twitter, images: twitter } as Twitter, | |
| 64 | metadata.metadataBase, | |
| 65 | ); | |
| 66 | metadata.twitter = resolvedTwitter; | |
| 67 | } | |
| 68 | ||
| 69 | if (openGraph) { | |
| 70 | const resolvedOpenGraph = resolveOpenGraph( | |
| 71 | { ...metadata.openGraph, images: openGraph } as OpenGraph, | |
| 72 | metadata.metadataBase, | |
| 73 | ); | |
| 74 | metadata.openGraph = resolvedOpenGraph; | |
| 75 | } | |
| 76 | if (manifest) { | |
| 77 | metadata.manifest = manifest; | |
| 78 | } | |
| 79 | ||
| 80 | return metadata; | |
| 81 | } | |
| 82 | ||
| 83 | // Merge the source metadata into the resolved target metadata. | |
| 84 | function merge({ | |
| 85 | target, | |
| 86 | source, | |
| 87 | staticFilesMetadata, | |
| 88 | titleTemplates, | |
| 89 | options, | |
| 90 | }: { | |
| 91 | target: ResolvedMetadata; | |
| 92 | source: Metadata | null; | |
| 93 | staticFilesMetadata: StaticMetadata; | |
| 94 | titleTemplates: { | |
| 95 | title: string | null; | |
| 96 | twitter: string | null; | |
| 97 | openGraph: string | null; | |
| 98 | }; | |
| 99 | options: MetadataAccumulationOptions; | |
| 100 | }) { | |
| 101 | // If there's override metadata, prefer it otherwise fallback to the default metadata. | |
| 102 | const metadataBase = typeof source?.metadataBase !== "undefined" | |
| 103 | ? source.metadataBase | |
| 104 | : target.metadataBase; | |
| 105 | for (const key_ in source) { | |
| 106 | const key = key_ as keyof Metadata; | |
| 107 | ||
| 108 | switch (key) { | |
| 109 | case "title": { | |
| 110 | target.title = resolveTitle(source.title, titleTemplates.title); | |
| 111 | break; | |
| 112 | } | |
| 113 | case "alternates": { | |
| 114 | target.alternates = resolveAlternates(source.alternates, metadataBase, { | |
| 115 | pathname: options.pathname, | |
| 116 | }); | |
| 117 | break; | |
| 118 | } | |
| 119 | case "openGraph": { | |
| 120 | target.openGraph = resolveOpenGraph(source.openGraph, metadataBase); | |
| 121 | if (target.openGraph) { | |
| 122 | target.openGraph.title = resolveTitle( | |
| 123 | target.openGraph.title, | |
| 124 | titleTemplates.openGraph, | |
| 125 | ); | |
| 126 | } | |
| 127 | break; | |
| 128 | } | |
| 129 | case "twitter": { | |
| 130 | target.twitter = resolveTwitter(source.twitter, metadataBase); | |
| 131 | if (target.twitter) { | |
| 132 | target.twitter.title = resolveTitle( | |
| 133 | target.twitter.title, | |
| 134 | titleTemplates.twitter, | |
| 135 | ); | |
| 136 | } | |
| 137 | break; | |
| 138 | } | |
| 139 | case "verification": | |
| 140 | target.verification = resolveVerification(source.verification); | |
| 141 | break; | |
| 142 | case "viewport": { | |
| 143 | target.viewport = resolveViewport(source.viewport); | |
| 144 | break; | |
| 145 | } | |
| 146 | case "icons": { | |
| 147 | target.icons = resolveIcons(source.icons); | |
| 148 | break; | |
| 149 | } | |
| 150 | case "appleWebApp": | |
| 151 | target.appleWebApp = resolveAppleWebApp(source.appleWebApp); | |
| 152 | break; | |
| 153 | case "appLinks": | |
| 154 | target.appLinks = resolveAppLinks(source.appLinks); | |
| 155 | break; | |
| 156 | case "robots": { | |
| 157 | target.robots = resolveRobots(source.robots); | |
| 158 | break; | |
| 159 | } | |
| 160 | case "themeColor": { | |
| 161 | target.themeColor = resolveThemeColor(source.themeColor); | |
| 162 | break; | |
| 163 | } | |
| 164 | case "archives": | |
| 165 | case "assets": | |
| 166 | case "bookmarks": | |
| 167 | case "keywords": | |
| 168 | case "authors": { | |
| 169 | // FIXME: type inferring | |
| 170 | // @ts-ignore | |
| 171 | target[key] = resolveAsArrayOrUndefined(source[key]) || null; | |
| 172 | break; | |
| 173 | } | |
| 174 | // directly assign fields that fallback to null | |
| 175 | case "applicationName": | |
| 176 | case "description": | |
| 177 | case "generator": | |
| 178 | case "creator": | |
| 179 | case "publisher": | |
| 180 | case "category": | |
| 181 | case "classification": | |
| 182 | case "referrer": | |
| 183 | case "colorScheme": | |
| 184 | case "itunes": | |
| 185 | case "formatDetection": | |
| 186 | case "manifest": | |
| 187 | // @ts-ignore TODO: support inferring | |
| 188 | target[key] = source[key] || null; | |
| 189 | break; | |
| 190 | case "other": | |
| 191 | target.other = Object.assign({}, target.other, source.other); | |
| 192 | break; | |
| 193 | case "metadataBase": | |
| 194 | target.metadataBase = metadataBase; | |
| 195 | break; | |
| 196 | default: | |
| 197 | break; | |
| 198 | } | |
| 199 | } | |
| 200 | mergeStaticMetadata(target, staticFilesMetadata); | |
| 201 | } | |
| 202 | ||
| 203 | async function getDefinedMetadata( | |
| 204 | mod: any, | |
| 205 | props: any, | |
| 206 | route: string, | |
| 207 | ): Promise<Metadata | MetadataResolver | null> { | |
| 208 | // Layer is a client component, we just skip it. It can't have metadata exported. | |
| 209 | // Return early to avoid accessing properties error for client references. | |
| 210 | if (isClientReference(mod)) { | |
| 211 | return null; | |
| 212 | } | |
| 213 | return ( | |
| 214 | (mod.generateMetadata | |
| 215 | ? (parent: ResolvingMetadata) => | |
| 216 | getTracer().trace( | |
| 217 | ResolveMetadataSpan.generateMetadata, | |
| 218 | { | |
| 219 | spanName: `generateMetadata ${route}`, | |
| 220 | attributes: { | |
| 221 | "next.page": route, | |
| 222 | }, | |
| 223 | }, | |
| 224 | () => mod.generateMetadata(props, parent), | |
| 225 | ) | |
| 226 | : mod.metadata) || null | |
| 227 | ); | |
| 228 | } | |
| 229 | ||
| 230 | async function collectStaticImagesFiles( | |
| 231 | metadata: ComponentsType["metadata"], | |
| 232 | props: any, | |
| 233 | type: keyof NonNullable<ComponentsType["metadata"]>, | |
| 234 | ) { | |
| 235 | if (!metadata?.[type]) return undefined; | |
| 236 | ||
| 237 | const iconPromises = metadata[type as "icon" | "apple"].map( | |
| 238 | async (imageModule: (p: any) => Promise<MetadataImageModule[]>) => | |
| 239 | interopDefault(await imageModule(props)), | |
| 240 | ); | |
| 241 | ||
| 242 | return iconPromises?.length > 0 | |
| 243 | ? (await Promise.all(iconPromises))?.flat() | |
| 244 | : undefined; | |
| 245 | } | |
| 246 | ||
| 247 | async function resolveStaticMetadata(components: ComponentsType, props: any) { | |
| 248 | const { metadata } = components; | |
| 249 | if (!metadata) return null; | |
| 250 | ||
| 251 | const [icon, apple, openGraph, twitter] = await Promise.all([ | |
| 252 | collectStaticImagesFiles(metadata, props, "icon"), | |
| 253 | collectStaticImagesFiles(metadata, props, "apple"), | |
| 254 | collectStaticImagesFiles(metadata, props, "openGraph"), | |
| 255 | collectStaticImagesFiles(metadata, props, "twitter"), | |
| 256 | ]); | |
| 257 | ||
| 258 | const staticMetadata = { | |
| 259 | icon, | |
| 260 | apple, | |
| 261 | openGraph, | |
| 262 | twitter, | |
| 263 | manifest: metadata.manifest, | |
| 264 | }; | |
| 265 | ||
| 266 | return staticMetadata; | |
| 267 | } | |
| 268 | ||
| 269 | // [layout.metadata, static files metadata] -> ... -> [page.metadata, static files metadata] | |
| 270 | export async function collectMetadata({ | |
| 271 | tree, | |
| 272 | metadataItems: array, | |
| 273 | props, | |
| 274 | route, | |
| 275 | }: { | |
| 276 | tree: LoaderTree; | |
| 277 | metadataItems: MetadataItems; | |
| 278 | props: any; | |
| 279 | route: string; | |
| 280 | }) { | |
| 281 | const [mod, modType] = await getLayoutOrPageModule(tree); | |
| 282 | ||
| 283 | if (modType) { | |
| 284 | route += `/${modType}`; | |
| 285 | } | |
| 286 | ||
| 287 | const staticFilesMetadata = await resolveStaticMetadata(tree[2], props); | |
| 288 | const metadataExport = mod | |
| 289 | ? await getDefinedMetadata(mod, props, route) | |
| 290 | : null; | |
| 291 | ||
| 292 | array.push([metadataExport, staticFilesMetadata]); | |
| 293 | } | |
| 294 | ||
| 295 | export async function resolveMetadata({ | |
| 296 | tree, | |
| 297 | parentParams, | |
| 298 | metadataItems, | |
| 299 | treePrefix = [], | |
| 300 | getDynamicParamFromSegment, | |
| 301 | searchParams, | |
| 302 | }: { | |
| 303 | tree: LoaderTree; | |
| 304 | parentParams: { [key: string]: any }; | |
| 305 | metadataItems: MetadataItems; | |
| 306 | /** Provided tree can be nested subtree, this argument says what is the path of such subtree */ | |
| 307 | treePrefix?: string[]; | |
| 308 | getDynamicParamFromSegment: GetDynamicParamFromSegment; | |
| 309 | searchParams: { [key: string]: any }; | |
| 310 | }): Promise<MetadataItems> { | |
| 311 | const [segment, parallelRoutes, { page }] = tree; | |
| 312 | const currentTreePrefix = [...treePrefix, segment]; | |
| 313 | const isPage = typeof page !== "undefined"; | |
| 314 | // Handle dynamic segment params. | |
| 315 | const segmentParam = getDynamicParamFromSegment(segment); | |
| 316 | /** | |
| 317 | * Create object holding the parent params and current params | |
| 318 | */ | |
| 319 | const currentParams = | |
| 320 | // Handle null case where dynamic param is optional | |
| 321 | segmentParam && segmentParam.value !== null | |
| 322 | ? { | |
| 323 | ...parentParams, | |
| 324 | [segmentParam.param]: segmentParam.value, | |
| 325 | } | |
| 326 | // Pass through parent params to children | |
| 327 | : parentParams; | |
| 328 | ||
| 329 | const layerProps = { | |
| 330 | params: currentParams, | |
| 331 | ...(isPage && { searchParams }), | |
| 332 | }; | |
| 333 | ||
| 334 | await collectMetadata({ | |
| 335 | tree, | |
| 336 | metadataItems, | |
| 337 | props: layerProps, | |
| 338 | route: currentTreePrefix | |
| 339 | // __PAGE__ shouldn't be shown in a route | |
| 340 | .filter((s) => s !== PAGE_SEGMENT_KEY) | |
| 341 | .join("/"), | |
| 342 | }); | |
| 343 | ||
| 344 | for (const key in parallelRoutes) { | |
| 345 | const childTree = parallelRoutes[key]; | |
| 346 | await resolveMetadata({ | |
| 347 | tree: childTree, | |
| 348 | metadataItems, | |
| 349 | parentParams: currentParams, | |
| 350 | treePrefix: currentTreePrefix, | |
| 351 | searchParams, | |
| 352 | getDynamicParamFromSegment, | |
| 353 | }); | |
| 354 | } | |
| 355 | ||
| 356 | return metadataItems; | |
| 357 | } | |
| 358 | ||
| 359 | type MetadataAccumulationOptions = { | |
| 360 | pathname: string; | |
| 361 | }; | |
| 362 | ||
| 363 | export async function accumulateMetadata( | |
| 364 | metadataItems: MetadataItems, | |
| 365 | options: MetadataAccumulationOptions, | |
| 366 | ): Promise<ResolvedMetadata> { | |
| 367 | const resolvedMetadata = createDefaultMetadata(); | |
| 368 | const resolvers: ((value: ResolvedMetadata) => void)[] = []; | |
| 369 | const generateMetadataResults: (Metadata | Promise<Metadata>)[] = []; | |
| 370 | ||
| 371 | let titleTemplates: { | |
| 372 | title: string | null; | |
| 373 | twitter: string | null; | |
| 374 | openGraph: string | null; | |
| 375 | } = { | |
| 376 | title: null, | |
| 377 | twitter: null, | |
| 378 | openGraph: null, | |
| 379 | }; | |
| 380 | ||
| 381 | // Loop over all metadata items again, merging synchronously any static object exports, | |
| 382 | // awaiting any static promise exports, and resolving parent metadata and awaiting any generated metadata | |
| 383 | ||
| 384 | let resolvingIndex = 0; | |
| 385 | for (let i = 0; i < metadataItems.length; i++) { | |
| 386 | const [metadataExport, staticFilesMetadata] = metadataItems[i]; | |
| 387 | let metadata: Metadata | null = null; | |
| 388 | if (typeof metadataExport === "function") { | |
| 389 | if (!resolvers.length) { | |
| 390 | for (let j = i; j < metadataItems.length; j++) { | |
| 391 | const [preloadMetadataExport] = metadataItems[j]; | |
| 392 | // call each `generateMetadata function concurrently and stash their resolver | |
| 393 | if (typeof preloadMetadataExport === "function") { | |
| 394 | generateMetadataResults.push( | |
| 395 | preloadMetadataExport( | |
| 396 | new Promise((resolve) => { | |
| 397 | resolvers.push(resolve); | |
| 398 | }), | |
| 399 | ), | |
| 400 | ); | |
| 401 | } | |
| 402 | } | |
| 403 | } | |
| 404 | ||
| 405 | const resolveParent = resolvers[resolvingIndex]; | |
| 406 | const generatedMetadata = generateMetadataResults[resolvingIndex++]; | |
| 407 | ||
| 408 | // In dev we clone and freeze to prevent relying on mutating resolvedMetadata directly. | |
| 409 | // In prod we just pass resolvedMetadata through without any copying. | |
| 410 | const currentResolvedMetadata: ResolvedMetadata = | |
| 411 | process.env.NODE_ENV === "development" | |
| 412 | ? Object.freeze( | |
| 413 | require( | |
| 414 | "next/dist/compiled/@edge-runtime/primitives/structured-clone", | |
| 415 | ).structuredClone( | |
| 416 | resolvedMetadata, | |
| 417 | ), | |
| 418 | ) | |
| 419 | : resolvedMetadata; | |
| 420 | ||
| 421 | // This resolve should unblock the generateMetadata function if it awaited the parent | |
| 422 | // argument. If it didn't await the parent argument it might already have a value since it was | |
| 423 | // called concurrently. Regardless we await the return value before continuing on to the next layer | |
| 424 | resolveParent(currentResolvedMetadata); | |
| 425 | metadata = generatedMetadata instanceof Promise | |
| 426 | ? await generatedMetadata | |
| 427 | : generatedMetadata; | |
| 428 | } else if (metadataExport !== null && typeof metadataExport === "object") { | |
| 429 | // This metadataExport is the object form | |
| 430 | metadata = metadataExport; | |
| 431 | } | |
| 432 | ||
| 433 | merge({ | |
| 434 | options, | |
| 435 | target: resolvedMetadata, | |
| 436 | source: metadata, | |
| 437 | staticFilesMetadata, | |
| 438 | titleTemplates, | |
| 439 | }); | |
| 440 | ||
| 441 | // If the layout is the same layer with page, skip the leaf layout and leaf page | |
| 442 | // The leaf layout and page are the last two items | |
| 443 | if (i < metadataItems.length - 2) { | |
| 444 | titleTemplates = { | |
| 445 | title: resolvedMetadata.title?.template || null, | |
| 446 | openGraph: resolvedMetadata.openGraph?.title?.template || null, | |
| 447 | twitter: resolvedMetadata.twitter?.title?.template || null, | |
| 448 | }; | |
| 449 | } | |
| 450 | } | |
| 451 | ||
| 452 | return resolvedMetadata; | |
| 453 | } |
framework/meta/nextjs/resolvers/resolve-basics.ts created+259| ... | ... | @@ -0,0 +1,259 @@ |
| 1 | import type { | |
| 2 | AlternateLinkDescriptor, | |
| 3 | ResolvedAlternateURLs, | |
| 4 | } from "../types/alternative-urls-types"; | |
| 5 | import type { Metadata, ResolvedMetadata } from "../types/metadata-interface"; | |
| 6 | import type { ResolvedVerification } from "../types/metadata-types"; | |
| 7 | import type { | |
| 8 | FieldResolver, | |
| 9 | FieldResolverWithMetadataBase, | |
| 10 | } from "../types/resolvers"; | |
| 11 | import type { Viewport } from "../types/extra-types"; | |
| 12 | import path from "path"; | |
| 13 | import { resolveAsArrayOrUndefined } from "../generate/utils"; | |
| 14 | import { resolveUrl } from "./resolve-url"; | |
| 15 | import { ViewPortKeys } from "../constants"; | |
| 16 | ||
| 17 | // Resolve with `metadataBase` if it's present, otherwise resolve with `pathname`. | |
| 18 | // Resolve with `pathname` if `url` is a relative path. | |
| 19 | function resolveAlternateUrl( | |
| 20 | url: string | URL, | |
| 21 | metadataBase: URL | null, | |
| 22 | pathname: string, | |
| 23 | ) { | |
| 24 | if (typeof url === "string" && url.startsWith("./")) { | |
| 25 | url = path.resolve(pathname, url); | |
| 26 | } else if (url instanceof URL) { | |
| 27 | url = new URL(pathname, url); | |
| 28 | } | |
| 29 | ||
| 30 | const result = metadataBase ? resolveUrl(url, metadataBase) : url; | |
| 31 | return result.toString(); | |
| 32 | } | |
| 33 | ||
| 34 | export const resolveThemeColor: FieldResolver<"themeColor"> = (themeColor) => { | |
| 35 | if (!themeColor) return null; | |
| 36 | const themeColorDescriptors: ResolvedMetadata["themeColor"] = []; | |
| 37 | ||
| 38 | resolveAsArrayOrUndefined(themeColor)?.forEach((descriptor) => { | |
| 39 | if (typeof descriptor === "string") { | |
| 40 | themeColorDescriptors.push({ color: descriptor }); | |
| 41 | } else if (typeof descriptor === "object") { | |
| 42 | themeColorDescriptors.push({ | |
| 43 | color: descriptor.color, | |
| 44 | media: descriptor.media, | |
| 45 | }); | |
| 46 | } | |
| 47 | }); | |
| 48 | ||
| 49 | return themeColorDescriptors; | |
| 50 | }; | |
| 51 | ||
| 52 | export const resolveViewport: FieldResolver<"viewport"> = (viewport) => { | |
| 53 | let resolved: ResolvedMetadata["viewport"] = null; | |
| 54 | ||
| 55 | if (typeof viewport === "string") { | |
| 56 | resolved = viewport; | |
| 57 | } else if (viewport) { | |
| 58 | resolved = ""; | |
| 59 | for (const viewportKey_ in ViewPortKeys) { | |
| 60 | const viewportKey = viewportKey_ as keyof Viewport; | |
| 61 | if (viewportKey in viewport) { | |
| 62 | let value = viewport[viewportKey]; | |
| 63 | if (typeof value === "boolean") value = value ? "yes" : "no"; | |
| 64 | if (resolved) resolved += ", "; | |
| 65 | resolved += `${ViewPortKeys[viewportKey]}=${value}`; | |
| 66 | } | |
| 67 | } | |
| 68 | } | |
| 69 | return resolved; | |
| 70 | }; | |
| 71 | ||
| 72 | function resolveUrlValuesOfObject( | |
| 73 | obj: | |
| 74 | | Record<string, string | URL | AlternateLinkDescriptor[] | null> | |
| 75 | | null | |
| 76 | | undefined, | |
| 77 | metadataBase: ResolvedMetadata["metadataBase"], | |
| 78 | pathname: string, | |
| 79 | ): null | Record<string, AlternateLinkDescriptor[]> { | |
| 80 | if (!obj) return null; | |
| 81 | ||
| 82 | const result: Record<string, AlternateLinkDescriptor[]> = {}; | |
| 83 | for (const [key, value] of Object.entries(obj)) { | |
| 84 | if (typeof value === "string" || value instanceof URL) { | |
| 85 | result[key] = [ | |
| 86 | { | |
| 87 | url: resolveAlternateUrl(value, metadataBase, pathname), // metadataBase ? resolveUrl(value, metadataBase)! : value, | |
| 88 | }, | |
| 89 | ]; | |
| 90 | } else { | |
| 91 | result[key] = []; | |
| 92 | value?.forEach((item, index) => { | |
| 93 | const url = resolveAlternateUrl(item.url, metadataBase, pathname); | |
| 94 | result[key][index] = { | |
| 95 | url, | |
| 96 | title: item.title, | |
| 97 | }; | |
| 98 | }); | |
| 99 | } | |
| 100 | } | |
| 101 | return result; | |
| 102 | } | |
| 103 | ||
| 104 | function resolveCanonicalUrl( | |
| 105 | urlOrDescriptor: string | URL | null | AlternateLinkDescriptor | undefined, | |
| 106 | metadataBase: URL | null, | |
| 107 | pathname: string, | |
| 108 | ): null | AlternateLinkDescriptor { | |
| 109 | if (!urlOrDescriptor) return null; | |
| 110 | ||
| 111 | const url = | |
| 112 | typeof urlOrDescriptor === "string" || urlOrDescriptor instanceof URL | |
| 113 | ? urlOrDescriptor | |
| 114 | : urlOrDescriptor.url; | |
| 115 | ||
| 116 | // Return string url because structureClone can't handle URL instance | |
| 117 | return { | |
| 118 | url: resolveAlternateUrl(url, metadataBase, pathname), | |
| 119 | }; | |
| 120 | } | |
| 121 | ||
| 122 | export const resolveAlternates: FieldResolverWithMetadataBase< | |
| 123 | "alternates", | |
| 124 | { pathname: string } | |
| 125 | > = (alternates, metadataBase, { pathname }) => { | |
| 126 | if (!alternates) return null; | |
| 127 | ||
| 128 | const canonical = resolveCanonicalUrl( | |
| 129 | alternates.canonical, | |
| 130 | metadataBase, | |
| 131 | pathname, | |
| 132 | ); | |
| 133 | const languages = resolveUrlValuesOfObject( | |
| 134 | alternates.languages, | |
| 135 | metadataBase, | |
| 136 | pathname, | |
| 137 | ); | |
| 138 | const media = resolveUrlValuesOfObject( | |
| 139 | alternates.media, | |
| 140 | metadataBase, | |
| 141 | pathname, | |
| 142 | ); | |
| 143 | const types = resolveUrlValuesOfObject( | |
| 144 | alternates.types, | |
| 145 | metadataBase, | |
| 146 | pathname, | |
| 147 | ); | |
| 148 | ||
| 149 | const result: ResolvedAlternateURLs = { | |
| 150 | canonical, | |
| 151 | languages, | |
| 152 | media, | |
| 153 | types, | |
| 154 | }; | |
| 155 | ||
| 156 | return result; | |
| 157 | }; | |
| 158 | ||
| 159 | const robotsKeys = [ | |
| 160 | "noarchive", | |
| 161 | "nosnippet", | |
| 162 | "noimageindex", | |
| 163 | "nocache", | |
| 164 | "notranslate", | |
| 165 | "indexifembedded", | |
| 166 | "nositelinkssearchbox", | |
| 167 | "unavailable_after", | |
| 168 | "max-video-preview", | |
| 169 | "max-image-preview", | |
| 170 | "max-snippet", | |
| 171 | ] as const; | |
| 172 | const resolveRobotsValue: (robots: Metadata["robots"]) => string | null = ( | |
| 173 | robots, | |
| 174 | ) => { | |
| 175 | if (!robots) return null; | |
| 176 | if (typeof robots === "string") return robots; | |
| 177 | ||
| 178 | const values: string[] = []; | |
| 179 | ||
| 180 | if (robots.index) values.push("index"); | |
| 181 | else if (typeof robots.index === "boolean") values.push("noindex"); | |
| 182 | ||
| 183 | if (robots.follow) values.push("follow"); | |
| 184 | else if (typeof robots.follow === "boolean") values.push("nofollow"); | |
| 185 | ||
| 186 | for (const key of robotsKeys) { | |
| 187 | const value = robots[key]; | |
| 188 | if (typeof value !== "undefined" && value !== false) { | |
| 189 | values.push(typeof value === "boolean" ? key : `${key}:${value}`); | |
| 190 | } | |
| 191 | } | |
| 192 | ||
| 193 | return values.join(", "); | |
| 194 | }; | |
| 195 | ||
| 196 | export const resolveRobots: FieldResolver<"robots"> = (robots) => { | |
| 197 | if (!robots) return null; | |
| 198 | return { | |
| 199 | basic: resolveRobotsValue(robots), | |
| 200 | googleBot: typeof robots !== "string" | |
| 201 | ? resolveRobotsValue(robots.googleBot) | |
| 202 | : null, | |
| 203 | }; | |
| 204 | }; | |
| 205 | ||
| 206 | const VerificationKeys = ["google", "yahoo", "yandex", "me", "other"] as const; | |
| 207 | export const resolveVerification: FieldResolver<"verification"> = ( | |
| 208 | verification, | |
| 209 | ) => { | |
| 210 | if (!verification) return null; | |
| 211 | const res: ResolvedVerification = {}; | |
| 212 | ||
| 213 | for (const key of VerificationKeys) { | |
| 214 | const value = verification[key]; | |
| 215 | if (value) { | |
| 216 | if (key === "other") { | |
| 217 | res.other = {}; | |
| 218 | for (const otherKey in verification.other) { | |
| 219 | const otherValue = resolveAsArrayOrUndefined( | |
| 220 | verification.other[otherKey], | |
| 221 | ); | |
| 222 | if (otherValue) res.other[otherKey] = otherValue; | |
| 223 | } | |
| 224 | } else res[key] = resolveAsArrayOrUndefined(value) as (string | number)[]; | |
| 225 | } | |
| 226 | } | |
| 227 | return res; | |
| 228 | }; | |
| 229 | ||
| 230 | export const resolveAppleWebApp: FieldResolver<"appleWebApp"> = (appWebApp) => { | |
| 231 | if (!appWebApp) return null; | |
| 232 | if (appWebApp === true) { | |
| 233 | return { | |
| 234 | capable: true, | |
| 235 | }; | |
| 236 | } | |
| 237 | ||
| 238 | const startupImages = appWebApp.startupImage | |
| 239 | ? resolveAsArrayOrUndefined(appWebApp.startupImage)?.map((item) => | |
| 240 | typeof item === "string" ? { url: item } : item | |
| 241 | ) | |
| 242 | : null; | |
| 243 | ||
| 244 | return { | |
| 245 | capable: "capable" in appWebApp ? !!appWebApp.capable : true, | |
| 246 | title: appWebApp.title || null, | |
| 247 | startupImage: startupImages, | |
| 248 | statusBarStyle: appWebApp.statusBarStyle || "default", | |
| 249 | }; | |
| 250 | }; | |
| 251 | ||
| 252 | export const resolveAppLinks: FieldResolver<"appLinks"> = (appLinks) => { | |
| 253 | if (!appLinks) return null; | |
| 254 | for (const key in appLinks) { | |
| 255 | // @ts-ignore // TODO: type infer | |
| 256 | appLinks[key] = resolveAsArrayOrUndefined(appLinks[key]); | |
| 257 | } | |
| 258 | return appLinks as ResolvedMetadata["appLinks"]; | |
| 259 | }; |
framework/meta/nextjs/resolvers/resolve-icons.ts created+34| ... | ... | @@ -0,0 +1,34 @@ |
| 1 | import type { ResolvedMetadata } from "../types/metadata-interface"; | |
| 2 | import type { Icon, IconDescriptor } from "../types/metadata-types"; | |
| 3 | import type { FieldResolver } from "../types/resolvers"; | |
| 4 | import { resolveAsArrayOrUndefined } from "../generate/utils"; | |
| 5 | import { isStringOrURL } from "./resolve-url"; | |
| 6 | import { IconKeys } from "../constants"; | |
| 7 | ||
| 8 | export function resolveIcon(icon: Icon): IconDescriptor { | |
| 9 | if (isStringOrURL(icon)) return { url: icon }; | |
| 10 | else if (Array.isArray(icon)) return icon; | |
| 11 | return icon; | |
| 12 | } | |
| 13 | ||
| 14 | export const resolveIcons: FieldResolver<"icons"> = (icons) => { | |
| 15 | if (!icons) { | |
| 16 | return null; | |
| 17 | } | |
| 18 | ||
| 19 | const resolved: ResolvedMetadata["icons"] = { | |
| 20 | icon: [], | |
| 21 | apple: [], | |
| 22 | }; | |
| 23 | if (Array.isArray(icons)) { | |
| 24 | resolved.icon = icons.map(resolveIcon).filter(Boolean); | |
| 25 | } else if (isStringOrURL(icons)) { | |
| 26 | resolved.icon = [resolveIcon(icons)]; | |
| 27 | } else { | |
| 28 | for (const key of IconKeys) { | |
| 29 | const values = resolveAsArrayOrUndefined(icons[key]); | |
| 30 | if (values) resolved[key] = values.map(resolveIcon); | |
| 31 | } | |
| 32 | } | |
| 33 | return resolved; | |
| 34 | }; |
framework/meta/nextjs/resolvers/resolve-opengraph.ts created+147| ... | ... | @@ -0,0 +1,147 @@ |
| 1 | import type { Metadata, ResolvedMetadata } from "../types/metadata-interface"; | |
| 2 | import type { | |
| 3 | OpenGraph, | |
| 4 | OpenGraphType, | |
| 5 | ResolvedOpenGraph, | |
| 6 | } from "../types/opengraph-types"; | |
| 7 | import type { FieldResolverWithMetadataBase } from "../types/resolvers"; | |
| 8 | import type { ResolvedTwitterMetadata, Twitter } from "../types/twitter-types"; | |
| 9 | import { resolveAsArrayOrUndefined } from "../generate/utils"; | |
| 10 | import { isStringOrURL, resolveUrl } from "./resolve-url"; | |
| 11 | ||
| 12 | const OgTypeFields = { | |
| 13 | article: ["authors", "tags"], | |
| 14 | song: ["albums", "musicians"], | |
| 15 | playlist: ["albums", "musicians"], | |
| 16 | radio: ["creators"], | |
| 17 | video: ["actors", "directors", "writers", "tags"], | |
| 18 | basic: [ | |
| 19 | "emails", | |
| 20 | "phoneNumbers", | |
| 21 | "faxNumbers", | |
| 22 | "alternateLocale", | |
| 23 | "audio", | |
| 24 | "videos", | |
| 25 | ], | |
| 26 | } as const; | |
| 27 | ||
| 28 | function resolveImages( | |
| 29 | images: Twitter["images"], | |
| 30 | metadataBase: ResolvedMetadata["metadataBase"], | |
| 31 | ): NonNullable<ResolvedMetadata["twitter"]>["images"]; | |
| 32 | function resolveImages( | |
| 33 | images: OpenGraph["images"], | |
| 34 | metadataBase: ResolvedMetadata["metadataBase"], | |
| 35 | ): NonNullable<ResolvedMetadata["openGraph"]>["images"]; | |
| 36 | function resolveImages( | |
| 37 | images: OpenGraph["images"] | Twitter["images"], | |
| 38 | metadataBase: ResolvedMetadata["metadataBase"], | |
| 39 | ): | |
| 40 | | NonNullable<ResolvedMetadata["twitter"]>["images"] | |
| 41 | | NonNullable<ResolvedMetadata["openGraph"]>["images"] { | |
| 42 | const resolvedImages = resolveAsArrayOrUndefined(images); | |
| 43 | resolvedImages?.forEach((item, index, array) => { | |
| 44 | if (isStringOrURL(item)) { | |
| 45 | array[index] = { | |
| 46 | url: resolveUrl(item, metadataBase)!, | |
| 47 | }; | |
| 48 | } else { | |
| 49 | // Update image descriptor url | |
| 50 | item.url = resolveUrl(item.url, metadataBase)!; | |
| 51 | } | |
| 52 | }); | |
| 53 | return resolvedImages; | |
| 54 | } | |
| 55 | ||
| 56 | function getFieldsByOgType(ogType: OpenGraphType | undefined) { | |
| 57 | switch (ogType) { | |
| 58 | case "article": | |
| 59 | case "book": | |
| 60 | return OgTypeFields.article; | |
| 61 | case "music.song": | |
| 62 | case "music.album": | |
| 63 | return OgTypeFields.song; | |
| 64 | case "music.playlist": | |
| 65 | return OgTypeFields.playlist; | |
| 66 | case "music.radio_station": | |
| 67 | return OgTypeFields.radio; | |
| 68 | case "video.movie": | |
| 69 | case "video.episode": | |
| 70 | return OgTypeFields.video; | |
| 71 | default: | |
| 72 | return OgTypeFields.basic; | |
| 73 | } | |
| 74 | } | |
| 75 | ||
| 76 | export const resolveOpenGraph: FieldResolverWithMetadataBase<"openGraph"> = ( | |
| 77 | openGraph: Metadata["openGraph"], | |
| 78 | metadataBase: ResolvedMetadata["metadataBase"], | |
| 79 | ) => { | |
| 80 | if (!openGraph) return null; | |
| 81 | ||
| 82 | const url = resolveUrl(openGraph.url, metadataBase); | |
| 83 | const resolved = { ...openGraph } as ResolvedOpenGraph; | |
| 84 | ||
| 85 | function assignProps(og: OpenGraph) { | |
| 86 | const ogType = og && "type" in og ? og.type : undefined; | |
| 87 | const keys = getFieldsByOgType(ogType); | |
| 88 | for (const k of keys) { | |
| 89 | const key = k as keyof ResolvedOpenGraph; | |
| 90 | if (key in og && key !== "url") { | |
| 91 | const value = og[key]; | |
| 92 | if (value) { | |
| 93 | const arrayValue = resolveAsArrayOrUndefined(value); /// TODO: improve typing inferring | |
| 94 | (resolved as any)[key] = arrayValue; | |
| 95 | } | |
| 96 | } | |
| 97 | } | |
| 98 | ||
| 99 | resolved.images = resolveImages(og.images, metadataBase); | |
| 100 | } | |
| 101 | ||
| 102 | assignProps(openGraph); | |
| 103 | ||
| 104 | resolved.url = url; | |
| 105 | ||
| 106 | return resolved; | |
| 107 | }; | |
| 108 | ||
| 109 | const TwitterBasicInfoKeys = [ | |
| 110 | "site", | |
| 111 | "siteId", | |
| 112 | "creator", | |
| 113 | "creatorId", | |
| 114 | "description", | |
| 115 | ] as const; | |
| 116 | ||
| 117 | export const resolveTwitter: FieldResolverWithMetadataBase<"twitter"> = ( | |
| 118 | twitter, | |
| 119 | metadataBase, | |
| 120 | ) => { | |
| 121 | if (!twitter) return null; | |
| 122 | const resolved = { | |
| 123 | ...twitter, | |
| 124 | card: "card" in twitter ? twitter.card : "summary", | |
| 125 | } as ResolvedTwitterMetadata; | |
| 126 | for (const infoKey of TwitterBasicInfoKeys) { | |
| 127 | resolved[infoKey] = twitter[infoKey] || null; | |
| 128 | } | |
| 129 | resolved.images = resolveImages(twitter.images, metadataBase); | |
| 130 | ||
| 131 | if ("card" in resolved) { | |
| 132 | switch (resolved.card) { | |
| 133 | case "player": { | |
| 134 | resolved.players = resolveAsArrayOrUndefined(resolved.players) || []; | |
| 135 | break; | |
| 136 | } | |
| 137 | case "app": { | |
| 138 | resolved.app = resolved.app || {}; | |
| 139 | break; | |
| 140 | } | |
| 141 | default: | |
| 142 | break; | |
| 143 | } | |
| 144 | } | |
| 145 | ||
| 146 | return resolved; | |
| 147 | }; |
framework/meta/nextjs/resolvers/resolve-title.ts created+39| ... | ... | @@ -0,0 +1,39 @@ |
| 1 | import type { Metadata } from "../types/metadata-interface"; | |
| 2 | import type { AbsoluteTemplateString } from "../types/metadata-types"; | |
| 3 | ||
| 4 | function resolveTitleTemplate( | |
| 5 | template: string | null | undefined, | |
| 6 | title: string, | |
| 7 | ) { | |
| 8 | return template ? template.replace(/%s/g, title) : title; | |
| 9 | } | |
| 10 | ||
| 11 | export function resolveTitle( | |
| 12 | title: Metadata["title"], | |
| 13 | stashedTemplate: string | null | undefined, | |
| 14 | ): AbsoluteTemplateString { | |
| 15 | let resolved; | |
| 16 | const template = typeof title !== "string" && title && "template" in title | |
| 17 | ? title.template | |
| 18 | : null; | |
| 19 | ||
| 20 | if (typeof title === "string") { | |
| 21 | resolved = resolveTitleTemplate(stashedTemplate, title); | |
| 22 | } else if (title) { | |
| 23 | if ("default" in title) { | |
| 24 | resolved = resolveTitleTemplate(stashedTemplate, title.default); | |
| 25 | } | |
| 26 | if ("absolute" in title && title.absolute) { | |
| 27 | resolved = title.absolute; | |
| 28 | } | |
| 29 | } | |
| 30 | ||
| 31 | if (title && typeof title !== "string") { | |
| 32 | return { | |
| 33 | template, | |
| 34 | absolute: resolved || "", | |
| 35 | }; | |
| 36 | } else { | |
| 37 | return { absolute: resolved || title || "", template }; | |
| 38 | } | |
| 39 | } |
framework/meta/nextjs/resolvers/resolve-url.ts created+38| ... | ... | @@ -0,0 +1,38 @@ |
| 1 | import path from "path"; | |
| 2 | import process from "node:process"; | |
| 3 | ||
| 4 | function isStringOrURL(icon: any): icon is string | URL { | |
| 5 | return typeof icon === "string" || icon instanceof URL; | |
| 6 | } | |
| 7 | ||
| 8 | function resolveUrl(url: null | undefined, metadataBase: URL | null): null; | |
| 9 | function resolveUrl(url: string | URL, metadataBase: URL | null): URL; | |
| 10 | function resolveUrl( | |
| 11 | url: string | URL | null | undefined, | |
| 12 | metadataBase: URL | null, | |
| 13 | ): URL | null; | |
| 14 | function resolveUrl( | |
| 15 | url: string | URL | null | undefined, | |
| 16 | metadataBase: URL | null, | |
| 17 | ): URL | null { | |
| 18 | if (url instanceof URL) return url; | |
| 19 | if (!url) return null; | |
| 20 | ||
| 21 | try { | |
| 22 | // If we can construct a URL instance from url, ignore metadataBase | |
| 23 | const parsedUrl = new URL(url); | |
| 24 | return parsedUrl; | |
| 25 | } catch (_) {} | |
| 26 | ||
| 27 | if (!metadataBase) { | |
| 28 | metadataBase = new URL(`http://localhost:${process.env.PORT || 3000}`); | |
| 29 | } | |
| 30 | ||
| 31 | // Handle relative or absolute paths | |
| 32 | const basePath = metadataBase.pathname || ""; | |
| 33 | const joinedPath = path.join(basePath, url); | |
| 34 | ||
| 35 | return new URL(joinedPath, metadataBase); | |
| 36 | } | |
| 37 | ||
| 38 | export { isStringOrURL, resolveUrl }; |
framework/meta/nextjs/types/alternative-urls-types.ts created+450| ... | ... | @@ -0,0 +1,450 @@ |
| 1 | // Reference: https://hreflang.org/what-is-a-valid-hreflang | |
| 2 | ||
| 3 | type LangCode = | |
| 4 | | "aa" | |
| 5 | | "ab" | |
| 6 | | "ae" | |
| 7 | | "af" | |
| 8 | | "ak" | |
| 9 | | "am" | |
| 10 | | "an" | |
| 11 | | "ar" | |
| 12 | | "as" | |
| 13 | | "av" | |
| 14 | | "ay" | |
| 15 | | "az" | |
| 16 | | "ba" | |
| 17 | | "be" | |
| 18 | | "bg" | |
| 19 | | "bh" | |
| 20 | | "bi" | |
| 21 | | "bm" | |
| 22 | | "bn" | |
| 23 | | "bo" | |
| 24 | | "br" | |
| 25 | | "bs" | |
| 26 | | "ca" | |
| 27 | | "ce" | |
| 28 | | "ch" | |
| 29 | | "co" | |
| 30 | | "cr" | |
| 31 | | "cs" | |
| 32 | | "cu" | |
| 33 | | "cv" | |
| 34 | | "cy" | |
| 35 | | "da" | |
| 36 | | "de" | |
| 37 | | "dv" | |
| 38 | | "dz" | |
| 39 | | "ee" | |
| 40 | | "el" | |
| 41 | | "en" | |
| 42 | | "eo" | |
| 43 | | "es" | |
| 44 | | "et" | |
| 45 | | "eu" | |
| 46 | | "fa" | |
| 47 | | "ff" | |
| 48 | | "fi" | |
| 49 | | "fj" | |
| 50 | | "fo" | |
| 51 | | "fr" | |
| 52 | | "fy" | |
| 53 | | "ga" | |
| 54 | | "gd" | |
| 55 | | "gl" | |
| 56 | | "gn" | |
| 57 | | "gu" | |
| 58 | | "gv" | |
| 59 | | "ha" | |
| 60 | | "he" | |
| 61 | | "hi" | |
| 62 | | "ho" | |
| 63 | | "hr" | |
| 64 | | "ht" | |
| 65 | | "hu" | |
| 66 | | "hy" | |
| 67 | | "hz" | |
| 68 | | "ia" | |
| 69 | | "id" | |
| 70 | | "ie" | |
| 71 | | "ig" | |
| 72 | | "ii" | |
| 73 | | "ik" | |
| 74 | | "io" | |
| 75 | | "is" | |
| 76 | | "it" | |
| 77 | | "iu" | |
| 78 | | "ja" | |
| 79 | | "jv" | |
| 80 | | "ka" | |
| 81 | | "kg" | |
| 82 | | "ki" | |
| 83 | | "kj" | |
| 84 | | "kk" | |
| 85 | | "kl" | |
| 86 | | "km" | |
| 87 | | "kn" | |
| 88 | | "ko" | |
| 89 | | "kr" | |
| 90 | | "ks" | |
| 91 | | "ku" | |
| 92 | | "kv" | |
| 93 | | "kw" | |
| 94 | | "ky" | |
| 95 | | "la" | |
| 96 | | "lb" | |
| 97 | | "lg" | |
| 98 | | "li" | |
| 99 | | "ln" | |
| 100 | | "lo" | |
| 101 | | "lt" | |
| 102 | | "lu" | |
| 103 | | "lv" | |
| 104 | | "mg" | |
| 105 | | "mh" | |
| 106 | | "mi" | |
| 107 | | "mk" | |
| 108 | | "ml" | |
| 109 | | "mn" | |
| 110 | | "mr" | |
| 111 | | "ms" | |
| 112 | | "mt" | |
| 113 | | "my" | |
| 114 | | "na" | |
| 115 | | "nb" | |
| 116 | | "nd" | |
| 117 | | "ne" | |
| 118 | | "ng" | |
| 119 | | "nl" | |
| 120 | | "nn" | |
| 121 | | "no" | |
| 122 | | "nr" | |
| 123 | | "nv" | |
| 124 | | "ny" | |
| 125 | | "oc" | |
| 126 | | "oj" | |
| 127 | | "om" | |
| 128 | | "or" | |
| 129 | | "os" | |
| 130 | | "pa" | |
| 131 | | "pi" | |
| 132 | | "pl" | |
| 133 | | "ps" | |
| 134 | | "pt" | |
| 135 | | "qu" | |
| 136 | | "rm" | |
| 137 | | "rn" | |
| 138 | | "ro" | |
| 139 | | "ru" | |
| 140 | | "rw" | |
| 141 | | "sa" | |
| 142 | | "sc" | |
| 143 | | "sd" | |
| 144 | | "se" | |
| 145 | | "sg" | |
| 146 | | "si" | |
| 147 | | "sk" | |
| 148 | | "sl" | |
| 149 | | "sm" | |
| 150 | | "sn" | |
| 151 | | "so" | |
| 152 | | "sq" | |
| 153 | | "sr" | |
| 154 | | "ss" | |
| 155 | | "st" | |
| 156 | | "su" | |
| 157 | | "sv" | |
| 158 | | "sw" | |
| 159 | | "ta" | |
| 160 | | "te" | |
| 161 | | "tg" | |
| 162 | | "th" | |
| 163 | | "ti" | |
| 164 | | "tk" | |
| 165 | | "tl" | |
| 166 | | "tn" | |
| 167 | | "to" | |
| 168 | | "tr" | |
| 169 | | "ts" | |
| 170 | | "tt" | |
| 171 | | "tw" | |
| 172 | | "ty" | |
| 173 | | "ug" | |
| 174 | | "uk" | |
| 175 | | "ur" | |
| 176 | | "uz" | |
| 177 | | "ve" | |
| 178 | | "vi" | |
| 179 | | "vo" | |
| 180 | | "wa" | |
| 181 | | "wo" | |
| 182 | | "xh" | |
| 183 | | "yi" | |
| 184 | | "yo" | |
| 185 | | "za" | |
| 186 | | "zh" | |
| 187 | | "zu" | |
| 188 | | "af-ZA" | |
| 189 | | "am-ET" | |
| 190 | | "ar-AE" | |
| 191 | | "ar-BH" | |
| 192 | | "ar-DZ" | |
| 193 | | "ar-EG" | |
| 194 | | "ar-IQ" | |
| 195 | | "ar-JO" | |
| 196 | | "ar-KW" | |
| 197 | | "ar-LB" | |
| 198 | | "ar-LY" | |
| 199 | | "ar-MA" | |
| 200 | | "arn-CL" | |
| 201 | | "ar-OM" | |
| 202 | | "ar-QA" | |
| 203 | | "ar-SA" | |
| 204 | | "ar-SD" | |
| 205 | | "ar-SY" | |
| 206 | | "ar-TN" | |
| 207 | | "ar-YE" | |
| 208 | | "as-IN" | |
| 209 | | "az-az" | |
| 210 | | "az-Cyrl-AZ" | |
| 211 | | "az-Latn-AZ" | |
| 212 | | "ba-RU" | |
| 213 | | "be-BY" | |
| 214 | | "bg-BG" | |
| 215 | | "bn-BD" | |
| 216 | | "bn-IN" | |
| 217 | | "bo-CN" | |
| 218 | | "br-FR" | |
| 219 | | "bs-Cyrl-BA" | |
| 220 | | "bs-Latn-BA" | |
| 221 | | "ca-ES" | |
| 222 | | "co-FR" | |
| 223 | | "cs-CZ" | |
| 224 | | "cy-GB" | |
| 225 | | "da-DK" | |
| 226 | | "de-AT" | |
| 227 | | "de-CH" | |
| 228 | | "de-DE" | |
| 229 | | "de-LI" | |
| 230 | | "de-LU" | |
| 231 | | "dsb-DE" | |
| 232 | | "dv-MV" | |
| 233 | | "el-CY" | |
| 234 | | "el-GR" | |
| 235 | | "en-029" | |
| 236 | | "en-AU" | |
| 237 | | "en-BZ" | |
| 238 | | "en-CA" | |
| 239 | | "en-cb" | |
| 240 | | "en-GB" | |
| 241 | | "en-IE" | |
| 242 | | "en-IN" | |
| 243 | | "en-JM" | |
| 244 | | "en-MT" | |
| 245 | | "en-MY" | |
| 246 | | "en-NZ" | |
| 247 | | "en-PH" | |
| 248 | | "en-SG" | |
| 249 | | "en-TT" | |
| 250 | | "en-US" | |
| 251 | | "en-ZA" | |
| 252 | | "en-ZW" | |
| 253 | | "es-AR" | |
| 254 | | "es-BO" | |
| 255 | | "es-CL" | |
| 256 | | "es-CO" | |
| 257 | | "es-CR" | |
| 258 | | "es-DO" | |
| 259 | | "es-EC" | |
| 260 | | "es-ES" | |
| 261 | | "es-GT" | |
| 262 | | "es-HN" | |
| 263 | | "es-MX" | |
| 264 | | "es-NI" | |
| 265 | | "es-PA" | |
| 266 | | "es-PE" | |
| 267 | | "es-PR" | |
| 268 | | "es-PY" | |
| 269 | | "es-SV" | |
| 270 | | "es-US" | |
| 271 | | "es-UY" | |
| 272 | | "es-VE" | |
| 273 | | "et-EE" | |
| 274 | | "eu-ES" | |
| 275 | | "fa-IR" | |
| 276 | | "fi-FI" | |
| 277 | | "fil-PH" | |
| 278 | | "fo-FO" | |
| 279 | | "fr-BE" | |
| 280 | | "fr-CA" | |
| 281 | | "fr-CH" | |
| 282 | | "fr-FR" | |
| 283 | | "fr-LU" | |
| 284 | | "fr-MC" | |
| 285 | | "fy-NL" | |
| 286 | | "ga-IE" | |
| 287 | | "gd-GB" | |
| 288 | | "gd-ie" | |
| 289 | | "gl-ES" | |
| 290 | | "gsw-FR" | |
| 291 | | "gu-IN" | |
| 292 | | "ha-Latn-NG" | |
| 293 | | "he-IL" | |
| 294 | | "hi-IN" | |
| 295 | | "hr-BA" | |
| 296 | | "hr-HR" | |
| 297 | | "hsb-DE" | |
| 298 | | "hu-HU" | |
| 299 | | "hy-AM" | |
| 300 | | "id-ID" | |
| 301 | | "ig-NG" | |
| 302 | | "ii-CN" | |
| 303 | | "in-ID" | |
| 304 | | "is-IS" | |
| 305 | | "it-CH" | |
| 306 | | "it-IT" | |
| 307 | | "iu-Cans-CA" | |
| 308 | | "iu-Latn-CA" | |
| 309 | | "iw-IL" | |
| 310 | | "ja-JP" | |
| 311 | | "ka-GE" | |
| 312 | | "kk-KZ" | |
| 313 | | "kl-GL" | |
| 314 | | "km-KH" | |
| 315 | | "kn-IN" | |
| 316 | | "kok-IN" | |
| 317 | | "ko-KR" | |
| 318 | | "ky-KG" | |
| 319 | | "lb-LU" | |
| 320 | | "lo-LA" | |
| 321 | | "lt-LT" | |
| 322 | | "lv-LV" | |
| 323 | | "mi-NZ" | |
| 324 | | "mk-MK" | |
| 325 | | "ml-IN" | |
| 326 | | "mn-MN" | |
| 327 | | "mn-Mong-CN" | |
| 328 | | "moh-CA" | |
| 329 | | "mr-IN" | |
| 330 | | "ms-BN" | |
| 331 | | "ms-MY" | |
| 332 | | "mt-MT" | |
| 333 | | "nb-NO" | |
| 334 | | "ne-NP" | |
| 335 | | "nl-BE" | |
| 336 | | "nl-NL" | |
| 337 | | "nn-NO" | |
| 338 | | "no-no" | |
| 339 | | "nso-ZA" | |
| 340 | | "oc-FR" | |
| 341 | | "or-IN" | |
| 342 | | "pa-IN" | |
| 343 | | "pl-PL" | |
| 344 | | "prs-AF" | |
| 345 | | "ps-AF" | |
| 346 | | "pt-BR" | |
| 347 | | "pt-PT" | |
| 348 | | "qut-GT" | |
| 349 | | "quz-BO" | |
| 350 | | "quz-EC" | |
| 351 | | "quz-PE" | |
| 352 | | "rm-CH" | |
| 353 | | "ro-mo" | |
| 354 | | "ro-RO" | |
| 355 | | "ru-mo" | |
| 356 | | "ru-RU" | |
| 357 | | "rw-RW" | |
| 358 | | "sah-RU" | |
| 359 | | "sa-IN" | |
| 360 | | "se-FI" | |
| 361 | | "se-NO" | |
| 362 | | "se-SE" | |
| 363 | | "si-LK" | |
| 364 | | "sk-SK" | |
| 365 | | "sl-SI" | |
| 366 | | "sma-NO" | |
| 367 | | "sma-SE" | |
| 368 | | "smj-NO" | |
| 369 | | "smj-SE" | |
| 370 | | "smn-FI" | |
| 371 | | "sms-FI" | |
| 372 | | "sq-AL" | |
| 373 | | "sr-BA" | |
| 374 | | "sr-CS" | |
| 375 | | "sr-Cyrl-BA" | |
| 376 | | "sr-Cyrl-CS" | |
| 377 | | "sr-Cyrl-ME" | |
| 378 | | "sr-Cyrl-RS" | |
| 379 | | "sr-Latn-BA" | |
| 380 | | "sr-Latn-CS" | |
| 381 | | "sr-Latn-ME" | |
| 382 | | "sr-Latn-RS" | |
| 383 | | "sr-ME" | |
| 384 | | "sr-RS" | |
| 385 | | "sr-sp" | |
| 386 | | "sv-FI" | |
| 387 | | "sv-SE" | |
| 388 | | "sw-KE" | |
| 389 | | "syr-SY" | |
| 390 | | "ta-IN" | |
| 391 | | "te-IN" | |
| 392 | | "tg-Cyrl-TJ" | |
| 393 | | "th-TH" | |
| 394 | | "tk-TM" | |
| 395 | | "tlh-QS" | |
| 396 | | "tn-ZA" | |
| 397 | | "tr-TR" | |
| 398 | | "tt-RU" | |
| 399 | | "tzm-Latn-DZ" | |
| 400 | | "ug-CN" | |
| 401 | | "uk-UA" | |
| 402 | | "ur-PK" | |
| 403 | | "uz-Cyrl-UZ" | |
| 404 | | "uz-Latn-UZ" | |
| 405 | | "uz-uz" | |
| 406 | | "vi-VN" | |
| 407 | | "wo-SN" | |
| 408 | | "xh-ZA" | |
| 409 | | "yo-NG" | |
| 410 | | "zh-CN" | |
| 411 | | "zh-HK" | |
| 412 | | "zh-MO" | |
| 413 | | "zh-SG" | |
| 414 | | "zh-TW" | |
| 415 | | "zu-ZA"; | |
| 416 | ||
| 417 | type UnmatchedLang = "x-default"; | |
| 418 | ||
| 419 | type HrefLang = LangCode | UnmatchedLang; | |
| 420 | ||
| 421 | type Languages<T> = { | |
| 422 | [s in HrefLang]?: T; | |
| 423 | }; | |
| 424 | ||
| 425 | export type AlternateLinkDescriptor = { | |
| 426 | title?: string; | |
| 427 | url: string | URL; | |
| 428 | }; | |
| 429 | ||
| 430 | export type AlternateURLs = { | |
| 431 | canonical?: null | string | URL | AlternateLinkDescriptor; | |
| 432 | languages?: Languages<null | string | URL | AlternateLinkDescriptor[]>; | |
| 433 | media?: { | |
| 434 | [media: string]: null | string | URL | AlternateLinkDescriptor[]; | |
| 435 | }; | |
| 436 | types?: { | |
| 437 | [types: string]: null | string | URL | AlternateLinkDescriptor[]; | |
| 438 | }; | |
| 439 | }; | |
| 440 | ||
| 441 | export type ResolvedAlternateURLs = { | |
| 442 | canonical: null | AlternateLinkDescriptor; | |
| 443 | languages: null | Languages<AlternateLinkDescriptor[]>; | |
| 444 | media: null | { | |
| 445 | [media: string]: null | AlternateLinkDescriptor[]; | |
| 446 | }; | |
| 447 | types: null | { | |
| 448 | [types: string]: null | AlternateLinkDescriptor[]; | |
| 449 | }; | |
| 450 | }; |
framework/meta/nextjs/types/extra-types.ts created+104| ... | ... | @@ -0,0 +1,104 @@ |
| 1 | // When rendering applink meta tags add a namespace tag before each array instance | |
| 2 | // if more than one member exists. | |
| 3 | // ref: https://developers.facebook.com/docs/applinks/metadata-reference | |
| 4 | ||
| 5 | export type AppLinks = { | |
| 6 | ios?: AppLinksApple | Array<AppLinksApple>; | |
| 7 | iphone?: AppLinksApple | Array<AppLinksApple>; | |
| 8 | ipad?: AppLinksApple | Array<AppLinksApple>; | |
| 9 | android?: AppLinksAndroid | Array<AppLinksAndroid>; | |
| 10 | windows_phone?: AppLinksWindows | Array<AppLinksWindows>; | |
| 11 | windows?: AppLinksWindows | Array<AppLinksWindows>; | |
| 12 | windows_universal?: AppLinksWindows | Array<AppLinksWindows>; | |
| 13 | web?: AppLinksWeb | Array<AppLinksWeb>; | |
| 14 | }; | |
| 15 | export type ResolvedAppLinks = { | |
| 16 | ios?: Array<AppLinksApple>; | |
| 17 | iphone?: Array<AppLinksApple>; | |
| 18 | ipad?: Array<AppLinksApple>; | |
| 19 | android?: Array<AppLinksAndroid>; | |
| 20 | windows_phone?: Array<AppLinksWindows>; | |
| 21 | windows?: Array<AppLinksWindows>; | |
| 22 | windows_universal?: Array<AppLinksWindows>; | |
| 23 | web?: Array<AppLinksWeb>; | |
| 24 | }; | |
| 25 | export type AppLinksApple = { | |
| 26 | url: string | URL; | |
| 27 | app_store_id?: string | number; | |
| 28 | app_name?: string; | |
| 29 | }; | |
| 30 | export type AppLinksAndroid = { | |
| 31 | package: string; | |
| 32 | url?: string | URL; | |
| 33 | class?: string; | |
| 34 | app_name?: string; | |
| 35 | }; | |
| 36 | export type AppLinksWindows = { | |
| 37 | url: string | URL; | |
| 38 | app_id?: string; | |
| 39 | app_name?: string; | |
| 40 | }; | |
| 41 | export type AppLinksWeb = { | |
| 42 | url: string | URL; | |
| 43 | should_fallback?: boolean; | |
| 44 | }; | |
| 45 | ||
| 46 | // Apple Itunes APp | |
| 47 | // https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners | |
| 48 | export type ItunesApp = { | |
| 49 | appId: string; | |
| 50 | appArgument?: string; | |
| 51 | }; | |
| 52 | ||
| 53 | // Viewport meta structure | |
| 54 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag | |
| 55 | // intentionally leaving out user-scalable, use a string if you want that behavior | |
| 56 | export type Viewport = { | |
| 57 | width?: string | number; | |
| 58 | height?: string | number; | |
| 59 | initialScale?: number; | |
| 60 | minimumScale?: number; | |
| 61 | maximumScale?: number; | |
| 62 | userScalable?: boolean; | |
| 63 | viewportFit?: "auto" | "cover" | "contain"; | |
| 64 | interactiveWidget?: "resizes-visual" | "resizes-content" | "overlays-content"; | |
| 65 | }; | |
| 66 | ||
| 67 | // Apple Web App | |
| 68 | // https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html | |
| 69 | // https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html | |
| 70 | export type AppleWebApp = { | |
| 71 | // default true | |
| 72 | capable?: boolean; | |
| 73 | title?: string; | |
| 74 | startupImage?: AppleImage | Array<AppleImage>; | |
| 75 | // default "default" | |
| 76 | statusBarStyle?: "default" | "black" | "black-translucent"; | |
| 77 | }; | |
| 78 | export type AppleImage = string | AppleImageDescriptor; | |
| 79 | export type AppleImageDescriptor = { | |
| 80 | url: string; | |
| 81 | media?: string; | |
| 82 | }; | |
| 83 | ||
| 84 | export type ResolvedAppleWebApp = { | |
| 85 | capable: boolean; | |
| 86 | title?: string | null; | |
| 87 | startupImage?: AppleImageDescriptor[] | null; | |
| 88 | statusBarStyle?: "default" | "black" | "black-translucent"; | |
| 89 | }; | |
| 90 | ||
| 91 | // Format Detection | |
| 92 | // This is a poorly specified metadata export type that is supposed to | |
| 93 | // control whether the device attempts to conver text that matches | |
| 94 | // certain formats into links for action. The most supported example | |
| 95 | // is how mobile devices detect phone numbers and make them into links | |
| 96 | // that can initiate a phone call | |
| 97 | // https://www.goodemailcode.com/email-code/template.html | |
| 98 | export type FormatDetection = { | |
| 99 | telephone?: boolean; | |
| 100 | date?: boolean; | |
| 101 | address?: boolean; | |
| 102 | email?: boolean; | |
| 103 | url?: boolean; | |
| 104 | }; |
framework/meta/nextjs/types/manifest-types.ts created+86| ... | ... | @@ -0,0 +1,86 @@ |
| 1 | export type Manifest = { | |
| 2 | background_color?: string; | |
| 3 | categories?: string[]; | |
| 4 | description?: string; | |
| 5 | display?: "fullscreen" | "standalone" | "minimal-ui" | "browser"; | |
| 6 | display_override?: string[]; | |
| 7 | icons?: { | |
| 8 | src: string; | |
| 9 | type?: string; | |
| 10 | sizes?: string; | |
| 11 | purpose?: "any" | "maskable" | "monochrome" | "badge"; | |
| 12 | }[]; | |
| 13 | id?: string; | |
| 14 | launch_handler?: { | |
| 15 | platform?: "windows" | "macos" | "linux"; | |
| 16 | url?: string; | |
| 17 | }; | |
| 18 | name?: string; | |
| 19 | orientation?: | |
| 20 | | "any" | |
| 21 | | "natural" | |
| 22 | | "landscape" | |
| 23 | | "portrait" | |
| 24 | | "portrait-primary" | |
| 25 | | "portrait-secondary" | |
| 26 | | "landscape-primary" | |
| 27 | | "landscape-secondary"; | |
| 28 | prefer_related_applications?: boolean; | |
| 29 | protocol_handlers?: { | |
| 30 | protocol: string; | |
| 31 | url: string; | |
| 32 | title?: string; | |
| 33 | }[]; | |
| 34 | related_applications?: { | |
| 35 | platform: string; | |
| 36 | url: string; | |
| 37 | id?: string; | |
| 38 | }[]; | |
| 39 | scope?: string; | |
| 40 | screenshots?: { | |
| 41 | src: string; | |
| 42 | type?: string; | |
| 43 | sizes?: string; | |
| 44 | }[]; | |
| 45 | serviceworker?: { | |
| 46 | src?: string; | |
| 47 | scope?: string; | |
| 48 | type?: string; | |
| 49 | update_via_cache?: "import" | "none" | "all"; | |
| 50 | }; | |
| 51 | share_target?: { | |
| 52 | action?: string; | |
| 53 | method?: "get" | "post"; | |
| 54 | enctype?: | |
| 55 | | "application/x-www-form-urlencoded" | |
| 56 | | "multipart/form-data" | |
| 57 | | "text/plain"; | |
| 58 | params?: { | |
| 59 | name: string; | |
| 60 | value: string; | |
| 61 | required?: boolean; | |
| 62 | }[]; | |
| 63 | url?: string; | |
| 64 | title?: string; | |
| 65 | text?: string; | |
| 66 | files?: { | |
| 67 | accept?: string[]; | |
| 68 | name?: string; | |
| 69 | }[]; | |
| 70 | }; | |
| 71 | short_name?: string; | |
| 72 | shortcuts?: { | |
| 73 | name: string; | |
| 74 | short_name?: string; | |
| 75 | description?: string; | |
| 76 | url: string; | |
| 77 | icons?: { | |
| 78 | src: string; | |
| 79 | type?: string; | |
| 80 | sizes?: string; | |
| 81 | purpose?: "any" | "maskable" | "monochrome" | "badge"; | |
| 82 | }[]; | |
| 83 | }[]; | |
| 84 | start_url?: string; | |
| 85 | theme_color?: string; | |
| 86 | }; |
framework/meta/nextjs/types/metadata-interface.ts created+566| ... | ... | @@ -0,0 +1,566 @@ |
| 1 | import type { | |
| 2 | AlternateURLs, | |
| 3 | ResolvedAlternateURLs, | |
| 4 | } from "./alternative-urls-types"; | |
| 5 | import type { | |
| 6 | AppleWebApp, | |
| 7 | AppLinks, | |
| 8 | FormatDetection, | |
| 9 | ItunesApp, | |
| 10 | ResolvedAppleWebApp, | |
| 11 | ResolvedAppLinks, | |
| 12 | Viewport, | |
| 13 | } from "./extra-types"; | |
| 14 | import type { | |
| 15 | AbsoluteTemplateString, | |
| 16 | Author, | |
| 17 | ColorSchemeEnum, | |
| 18 | DeprecatedMetadataFields, | |
| 19 | Icon, | |
| 20 | Icons, | |
| 21 | IconURL, | |
| 22 | ReferrerEnum, | |
| 23 | ResolvedIcons, | |
| 24 | ResolvedRobots, | |
| 25 | ResolvedVerification, | |
| 26 | Robots, | |
| 27 | TemplateString, | |
| 28 | ThemeColorDescriptor, | |
| 29 | Verification, | |
| 30 | } from "./metadata-types"; | |
| 31 | import type { Manifest as ManifestFile } from "./manifest-types"; | |
| 32 | import type { OpenGraph, ResolvedOpenGraph } from "./opengraph-types"; | |
| 33 | import type { ResolvedTwitterMetadata, Twitter } from "./twitter-types"; | |
| 34 | ||
| 35 | /** | |
| 36 | * Metadata interface to describe all the metadata fields that can be set in a document. | |
| 37 | * @interface | |
| 38 | */ | |
| 39 | interface Metadata extends DeprecatedMetadataFields { | |
| 40 | /** | |
| 41 | * The base path and origin for absolute urls for various metadata links such as OpenGraph images. | |
| 42 | */ | |
| 43 | metadataBase?: null | URL; | |
| 44 | ||
| 45 | /** | |
| 46 | * The document title. | |
| 47 | * @example | |
| 48 | * ```tsx | |
| 49 | * "My Blog" | |
| 50 | * <title>My Blog</title> | |
| 51 | * | |
| 52 | * { default: "Dashboard", template: "%s | My Website" } | |
| 53 | * <title>Dashboard | My Website</title> | |
| 54 | * | |
| 55 | * { absolute: "My Blog", template: "%s | My Website" } | |
| 56 | * <title>My Blog</title> | |
| 57 | * ``` | |
| 58 | */ | |
| 59 | title?: null | string | TemplateString; | |
| 60 | ||
| 61 | /** | |
| 62 | * The document description, and optionally the OpenGraph and twitter descriptions. | |
| 63 | * @example | |
| 64 | * ```tsx | |
| 65 | * "My Blog Description" | |
| 66 | * <meta name="description" content="My Blog Description" /> | |
| 67 | * ``` | |
| 68 | */ | |
| 69 | description?: null | string; | |
| 70 | ||
| 71 | // Standard metadata names | |
| 72 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name | |
| 73 | ||
| 74 | /** | |
| 75 | * The application name. | |
| 76 | * @example | |
| 77 | * ```tsx | |
| 78 | * "My Blog" | |
| 79 | * <meta name="application-name" content="My Blog" /> | |
| 80 | * ``` | |
| 81 | */ | |
| 82 | applicationName?: null | string; | |
| 83 | ||
| 84 | /** | |
| 85 | * The authors of the document. | |
| 86 | * @example | |
| 87 | * ```tsx | |
| 88 | * [{ name: "Next.js Team", url: "https://nextjs.org" }] | |
| 89 | * | |
| 90 | * <meta name="author" content="Next.js Team" /> | |
| 91 | * <link rel="author" href="https://nextjs.org" /> | |
| 92 | * ``` | |
| 93 | */ | |
| 94 | authors?: null | Author | Array<Author>; | |
| 95 | ||
| 96 | /** | |
| 97 | * The generator used for the document. | |
| 98 | * @example | |
| 99 | * ```tsx | |
| 100 | * "Next.js" | |
| 101 | * | |
| 102 | * <meta name="generator" content="Next.js" /> | |
| 103 | * ``` | |
| 104 | */ | |
| 105 | generator?: null | string; | |
| 106 | ||
| 107 | /** | |
| 108 | * The keywords for the document. If an array is provided, it will be flattened into a single tag with comma separation. | |
| 109 | * @example | |
| 110 | * ```tsx | |
| 111 | * "nextjs, react, blog" | |
| 112 | * <meta name="keywords" content="nextjs, react, blog" /> | |
| 113 | * | |
| 114 | * ["react", "server components"] | |
| 115 | * <meta name="keywords" content="react, server components" /> | |
| 116 | * ``` | |
| 117 | */ | |
| 118 | keywords?: null | string | Array<string>; | |
| 119 | ||
| 120 | /** | |
| 121 | * The referrer setting for the document. | |
| 122 | * @example | |
| 123 | * ```tsx | |
| 124 | * "origin" | |
| 125 | * <meta name="referrer" content="origin" /> | |
| 126 | * ``` | |
| 127 | */ | |
| 128 | referrer?: null | ReferrerEnum; | |
| 129 | ||
| 130 | /** | |
| 131 | * The theme color for the document. | |
| 132 | * @example | |
| 133 | * ```tsx | |
| 134 | * "#000000" | |
| 135 | * <meta name="theme-color" content="#000000" /> | |
| 136 | * | |
| 137 | * { media: "(prefers-color-scheme: dark)", color: "#000000" } | |
| 138 | * <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000" /> | |
| 139 | * | |
| 140 | * [ | |
| 141 | * { media: "(prefers-color-scheme: dark)", color: "#000000" }, | |
| 142 | * { media: "(prefers-color-scheme: light)", color: "#ffffff" } | |
| 143 | * ] | |
| 144 | * <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000" /> | |
| 145 | * <meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" /> | |
| 146 | * ``` | |
| 147 | */ | |
| 148 | themeColor?: null | string | ThemeColorDescriptor | ThemeColorDescriptor[]; | |
| 149 | ||
| 150 | /** | |
| 151 | * The color scheme for the document. | |
| 152 | * @example | |
| 153 | * ```tsx | |
| 154 | * "dark" | |
| 155 | * <meta name="color-scheme" content="dark" /> | |
| 156 | * ``` | |
| 157 | */ | |
| 158 | colorScheme?: null | ColorSchemeEnum; | |
| 159 | ||
| 160 | /** | |
| 161 | * The viewport setting for the document. | |
| 162 | * @example | |
| 163 | * ```tsx | |
| 164 | * "width=device-width, initial-scale=1" | |
| 165 | * <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| 166 | * | |
| 167 | * { width: "device-width", initialScale: 1 } | |
| 168 | * <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| 169 | * ``` | |
| 170 | */ | |
| 171 | viewport?: null | string | Viewport; | |
| 172 | ||
| 173 | /** | |
| 174 | * The creator of the document. | |
| 175 | * @example | |
| 176 | * ```tsx | |
| 177 | * "Next.js Team" | |
| 178 | * <meta name="creator" content="Next.js Team" /> | |
| 179 | * ``` | |
| 180 | */ | |
| 181 | creator?: null | string; | |
| 182 | ||
| 183 | /** | |
| 184 | * The publisher of the document. | |
| 185 | * @example | |
| 186 | * | |
| 187 | * ```tsx | |
| 188 | * "Vercel" | |
| 189 | * <meta name="publisher" content="Vercel" /> | |
| 190 | * ``` | |
| 191 | */ | |
| 192 | publisher?: null | string; | |
| 193 | ||
| 194 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name#other_metadata_names | |
| 195 | ||
| 196 | /** | |
| 197 | * The robots setting for the document. | |
| 198 | * | |
| 199 | * @see https://developer.mozilla.org/en-US/docs/Glossary/Robots.txt | |
| 200 | * @example | |
| 201 | * ```tsx | |
| 202 | * "index, follow" | |
| 203 | * <meta name="robots" content="index, follow" /> | |
| 204 | * | |
| 205 | * { index: false, follow: false } | |
| 206 | * <meta name="robots" content="noindex, nofollow" /> | |
| 207 | * ``` | |
| 208 | */ | |
| 209 | robots?: null | string | Robots; | |
| 210 | ||
| 211 | /** | |
| 212 | * The canonical and alternate URLs for the document. | |
| 213 | * @example | |
| 214 | * ```tsx | |
| 215 | * { canonical: "https://example.com" } | |
| 216 | * <link rel="canonical" href="https://example.com" /> | |
| 217 | * | |
| 218 | * { canonical: "https://example.com", hreflang: { "en-US": "https://example.com/en-US" } } | |
| 219 | * <link rel="canonical" href="https://example.com" /> | |
| 220 | * <link rel="alternate" href="https://example.com/en-US" hreflang="en-US" /> | |
| 221 | * ``` | |
| 222 | * | |
| 223 | * Multiple titles example for alternate URLs except `canonical`: | |
| 224 | * ```tsx | |
| 225 | * { | |
| 226 | * canonical: "https://example.com", | |
| 227 | * types: { | |
| 228 | * 'application/rss+xml': [ | |
| 229 | * { url: 'blog.rss', title: 'rss' }, | |
| 230 | * { url: 'blog/js.rss', title: 'js title' }, | |
| 231 | * ], | |
| 232 | * }, | |
| 233 | * } | |
| 234 | * <link rel="canonical" href="https://example.com" /> | |
| 235 | * <link rel="alternate" href="https://example.com/blog.rss" type="application/rss+xml" title="rss" /> | |
| 236 | * <link rel="alternate" href="https://example.com/blog/js.rss" type="application/rss+xml" title="js title" /> | |
| 237 | * ``` | |
| 238 | */ | |
| 239 | alternates?: null | AlternateURLs; | |
| 240 | ||
| 241 | /** | |
| 242 | * The icons for the document. Defaults to rel="icon". | |
| 243 | * | |
| 244 | * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#attr-icon | |
| 245 | * @example | |
| 246 | * ```tsx | |
| 247 | * "https://example.com/icon.png" | |
| 248 | * <link rel="icon" href="https://example.com/icon.png" /> | |
| 249 | * | |
| 250 | * { icon: "https://example.com/icon.png", apple: "https://example.com/apple-icon.png" } | |
| 251 | * <link rel="icon" href="https://example.com/icon.png" /> | |
| 252 | * <link rel="apple-touch-icon" href="https://example.com/apple-icon.png" /> | |
| 253 | * | |
| 254 | * [{ rel: "icon", url: "https://example.com/icon.png" }, { rel: "apple-touch-icon", url: "https://example.com/apple-icon.png" }] | |
| 255 | * <link rel="icon" href="https://example.com/icon.png" /> | |
| 256 | * <link rel="apple-touch-icon" href="https://example.com/apple-icon.png" /> | |
| 257 | * ``` | |
| 258 | */ | |
| 259 | icons?: null | IconURL | Array<Icon> | Icons; | |
| 260 | ||
| 261 | /** | |
| 262 | * A web application manifest, as defined in the Web Application Manifest specification. | |
| 263 | * | |
| 264 | * @see https://developer.mozilla.org/en-US/docs/Web/Manifest | |
| 265 | * @example | |
| 266 | * ```tsx | |
| 267 | * "https://example.com/manifest.json" | |
| 268 | * <link rel="manifest" href="https://example.com/manifest.json" /> | |
| 269 | * ``` | |
| 270 | */ | |
| 271 | manifest?: null | string | URL; | |
| 272 | ||
| 273 | /** | |
| 274 | * The Open Graph metadata for the document. | |
| 275 | * | |
| 276 | * @see https://ogp.me | |
| 277 | * @example | |
| 278 | * ```tsx | |
| 279 | * { | |
| 280 | * type: "website", | |
| 281 | * url: "https://example.com", | |
| 282 | * title: "My Website", | |
| 283 | * description: "My Website Description", | |
| 284 | * siteName: "My Website", | |
| 285 | * images: [{ | |
| 286 | * url: "https://example.com/og.png", | |
| 287 | * }], | |
| 288 | * } | |
| 289 | * | |
| 290 | * <meta property="og:type" content="website" /> | |
| 291 | * <meta property="og:url" content="https://example.com" /> | |
| 292 | * <meta property="og:site_name" content="My Website" /> | |
| 293 | * <meta property="og:title" content="My Website" /> | |
| 294 | * <meta property="og:description" content="My Website Description" /> | |
| 295 | * <meta property="og:image" content="https://example.com/og.png" /> | |
| 296 | * ``` | |
| 297 | */ | |
| 298 | openGraph?: null | OpenGraph; | |
| 299 | ||
| 300 | /** | |
| 301 | * The Twitter metadata for the document. | |
| 302 | * @example | |
| 303 | * ```tsx | |
| 304 | * { card: "summary_large_image", site: "@site", creator: "@creator", "images": "https://example.com/og.png" } | |
| 305 | * | |
| 306 | * <meta name="twitter:card" content="summary_large_image" /> | |
| 307 | * <meta name="twitter:site" content="@site" /> | |
| 308 | * <meta name="twitter:creator" content="@creator" /> | |
| 309 | * <meta name="twitter:title" content="My Website" /> | |
| 310 | * <meta name="twitter:description" content="My Website Description" /> | |
| 311 | * <meta name="twitter:image" content="https://example.com/og.png" /> | |
| 312 | * ``` | |
| 313 | */ | |
| 314 | twitter?: null | Twitter; | |
| 315 | ||
| 316 | /** | |
| 317 | * The common verification tokens for the document. | |
| 318 | * @example | |
| 319 | * ```tsx | |
| 320 | * { verification: { google: "1234567890", yandex: "1234567890", "me": "1234567890" } } | |
| 321 | * <meta name="google-site-verification" content="1234567890" /> | |
| 322 | * <meta name="yandex-verification" content="1234567890" /> | |
| 323 | * <meta name="me" content="@me" /> | |
| 324 | * ``` | |
| 325 | */ | |
| 326 | verification?: Verification; | |
| 327 | ||
| 328 | /** | |
| 329 | * The Apple web app metadata for the document. | |
| 330 | * | |
| 331 | * @see https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html | |
| 332 | * @example | |
| 333 | * ```tsx | |
| 334 | * { capable: true, title: "My Website", statusBarStyle: "black-translucent" } | |
| 335 | * <meta name="apple-mobile-web-app-capable" content="yes" /> | |
| 336 | * <meta name="apple-mobile-web-app-title" content="My Website" /> | |
| 337 | * <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> | |
| 338 | * ``` | |
| 339 | */ | |
| 340 | appleWebApp?: null | boolean | AppleWebApp; | |
| 341 | ||
| 342 | /** | |
| 343 | * Indicates if devices should try to interpret various formats and make actionable links out of them. For example it controles | |
| 344 | * if telephone numbers on mobile that can be clicked to dial or not. | |
| 345 | * @example | |
| 346 | * ```tsx | |
| 347 | * { telephone: false } | |
| 348 | * <meta name="format-detection" content="telephone=no" /> | |
| 349 | * ``` | |
| 350 | */ | |
| 351 | formatDetection?: null | FormatDetection; | |
| 352 | ||
| 353 | /** | |
| 354 | * The metadata for the iTunes App. | |
| 355 | * It adds the `name="apple-itunes-app"` meta tag. | |
| 356 | * | |
| 357 | * @example | |
| 358 | * ```tsx | |
| 359 | * { app: { id: "123456789", affiliateData: "123456789", appArguments: "123456789" } } | |
| 360 | * <meta name="apple-itunes-app" content="app-id=123456789, affiliate-data=123456789, app-arguments=123456789" /> | |
| 361 | * ``` | |
| 362 | */ | |
| 363 | itunes?: null | ItunesApp; | |
| 364 | ||
| 365 | /** | |
| 366 | * A brief description of what this web-page is about. Not recommended, superseded by description. | |
| 367 | * It adds the `name="abstract"` meta tag. | |
| 368 | * | |
| 369 | * @see https://www.metatags.org/all-meta-tags-overview/meta-name-abstract/ | |
| 370 | * @example | |
| 371 | * ```tsx | |
| 372 | * "My Website Description" | |
| 373 | * <meta name="abstract" content="My Website Description" /> | |
| 374 | * ``` | |
| 375 | */ | |
| 376 | abstract?: null | string; | |
| 377 | ||
| 378 | /** | |
| 379 | * The Facebook AppLinks metadata for the document. | |
| 380 | * @example | |
| 381 | * ```tsx | |
| 382 | * { ios: { appStoreId: "123456789", url: "https://example.com" }, android: { packageName: "com.example", url: "https://example.com" } } | |
| 383 | * | |
| 384 | * <meta property="al:ios:app_store_id" content="123456789" /> | |
| 385 | * <meta property="al:ios:url" content="https://example.com" /> | |
| 386 | * <meta property="al:android:package" content="com.example" /> | |
| 387 | * <meta property="al:android:url" content="https://example.com" /> | |
| 388 | * ``` | |
| 389 | */ | |
| 390 | appLinks?: null | AppLinks; | |
| 391 | ||
| 392 | /** | |
| 393 | * The archives link rel property. | |
| 394 | * @example | |
| 395 | * ```tsx | |
| 396 | * { archives: "https://example.com/archives" } | |
| 397 | * <link rel="archives" href="https://example.com/archives" /> | |
| 398 | * ``` | |
| 399 | */ | |
| 400 | archives?: null | string | Array<string>; | |
| 401 | ||
| 402 | /** | |
| 403 | * The assets link rel property. | |
| 404 | * @example | |
| 405 | * ```tsx | |
| 406 | * "https://example.com/assets" | |
| 407 | * <link rel="assets" href="https://example.com/assets" /> | |
| 408 | * ``` | |
| 409 | */ | |
| 410 | assets?: null | string | Array<string>; | |
| 411 | ||
| 412 | /** | |
| 413 | * The bookmarks link rel property. | |
| 414 | * @example | |
| 415 | * ```tsx | |
| 416 | * "https://example.com/bookmarks" | |
| 417 | * <link rel="bookmarks" href="https://example.com/bookmarks" /> | |
| 418 | * ``` | |
| 419 | */ | |
| 420 | bookmarks?: null | string | Array<string>; // This is technically against HTML spec but is used in wild | |
| 421 | ||
| 422 | // meta name properties | |
| 423 | ||
| 424 | /** | |
| 425 | * The category meta name property. | |
| 426 | * @example | |
| 427 | * ```tsx | |
| 428 | * "My Category" | |
| 429 | * <meta name="category" content="My Category" /> | |
| 430 | * ``` | |
| 431 | */ | |
| 432 | category?: null | string; | |
| 433 | ||
| 434 | /** | |
| 435 | * The classification meta name property. | |
| 436 | * @example | |
| 437 | * ```tsx | |
| 438 | * "My Classification" | |
| 439 | * <meta name="classification" content="My Classification" /> | |
| 440 | * ``` | |
| 441 | */ | |
| 442 | classification?: null | string; | |
| 443 | ||
| 444 | /** | |
| 445 | * Arbitrary name/value pairs for the document. | |
| 446 | */ | |
| 447 | other?: { | |
| 448 | [name: string]: string | number | Array<string | number>; | |
| 449 | } & DeprecatedMetadataFields; | |
| 450 | } | |
| 451 | ||
| 452 | interface ResolvedMetadata extends DeprecatedMetadataFields { | |
| 453 | // origin and base path for absolute urls for various metadata links such as | |
| 454 | // opengraph-image | |
| 455 | metadataBase: null | URL; | |
| 456 | ||
| 457 | // The Document title and template if defined | |
| 458 | title: null | AbsoluteTemplateString; | |
| 459 | ||
| 460 | // The Document description, and optionally the opengraph and twitter descriptions | |
| 461 | description: null | string; | |
| 462 | ||
| 463 | // Standard metadata names | |
| 464 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name | |
| 465 | applicationName: null | string; | |
| 466 | authors: null | Array<Author>; | |
| 467 | generator: null | string; | |
| 468 | // if you provide an array it will be flattened into a single tag with comma separation | |
| 469 | keywords: null | Array<string>; | |
| 470 | referrer: null | ReferrerEnum; | |
| 471 | themeColor: null | ThemeColorDescriptor[]; | |
| 472 | colorScheme: null | ColorSchemeEnum; | |
| 473 | viewport: null | string; | |
| 474 | creator: null | string; | |
| 475 | publisher: null | string; | |
| 476 | ||
| 477 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name#other_metadata_names | |
| 478 | robots: null | ResolvedRobots; | |
| 479 | ||
| 480 | // The canonical and alternate URLs for this location | |
| 481 | alternates: null | ResolvedAlternateURLs; | |
| 482 | ||
| 483 | // Defaults to rel="icon" but the Icons type can be used | |
| 484 | // to get more specific about rel types | |
| 485 | icons: null | ResolvedIcons; | |
| 486 | ||
| 487 | openGraph: null | ResolvedOpenGraph; | |
| 488 | ||
| 489 | manifest: null | string | URL; | |
| 490 | ||
| 491 | twitter: null | ResolvedTwitterMetadata; | |
| 492 | ||
| 493 | // common verification tokens | |
| 494 | verification: null | ResolvedVerification; | |
| 495 | ||
| 496 | // Apple web app metadata | |
| 497 | // https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html | |
| 498 | appleWebApp: null | ResolvedAppleWebApp; | |
| 499 | ||
| 500 | // Should devices try to interpret various formats and make actionable links | |
| 501 | // out of them? The canonical example is telephone numbers on mobile that can | |
| 502 | // be clicked to dial | |
| 503 | formatDetection: null | FormatDetection; | |
| 504 | ||
| 505 | // meta name="apple-itunes-app" | |
| 506 | itunes: null | ItunesApp; | |
| 507 | ||
| 508 | // meta name="abstract" | |
| 509 | // A brief description of what this web-page is about. | |
| 510 | // Not recommended, superceded by description. | |
| 511 | // https://www.metatags.org/all-meta-tags-overview/meta-name-abstract/ | |
| 512 | abstract: null | string; | |
| 513 | ||
| 514 | // Facebook AppLinks | |
| 515 | appLinks: null | ResolvedAppLinks; | |
| 516 | ||
| 517 | // link rel properties | |
| 518 | archives: null | Array<string>; | |
| 519 | assets: null | Array<string>; | |
| 520 | bookmarks: null | Array<string>; // This is technically against HTML spec but is used in wild | |
| 521 | ||
| 522 | // meta name properties | |
| 523 | category: null | string; | |
| 524 | classification: null | string; | |
| 525 | ||
| 526 | // Arbitrary name/value pairs | |
| 527 | other: | |
| 528 | | null | |
| 529 | | ({ | |
| 530 | [name: string]: string | number | Array<string | number>; | |
| 531 | } & DeprecatedMetadataFields); | |
| 532 | } | |
| 533 | ||
| 534 | type RobotsFile = { | |
| 535 | // Apply rules for all | |
| 536 | rules: | |
| 537 | | { | |
| 538 | userAgent?: string | string[]; | |
| 539 | allow?: string | string[]; | |
| 540 | disallow?: string | string[]; | |
| 541 | crawlDelay?: number; | |
| 542 | } | |
| 543 | // Apply rules for specific user agents | |
| 544 | | Array<{ | |
| 545 | userAgent: string | string[]; | |
| 546 | allow?: string | string[]; | |
| 547 | disallow?: string | string[]; | |
| 548 | crawlDelay?: number; | |
| 549 | }>; | |
| 550 | sitemap?: string | string[]; | |
| 551 | host?: string; | |
| 552 | }; | |
| 553 | ||
| 554 | type SitemapFile = Array<{ | |
| 555 | url: string; | |
| 556 | lastModified?: string | Date; | |
| 557 | }>; | |
| 558 | ||
| 559 | type ResolvingMetadata = Promise<ResolvedMetadata>; | |
| 560 | declare namespace MetadataRoute { | |
| 561 | export type Robots = RobotsFile; | |
| 562 | export type Sitemap = SitemapFile; | |
| 563 | export type Manifest = ManifestFile; | |
| 564 | } | |
| 565 | ||
| 566 | export { Metadata, MetadataRoute, ResolvedMetadata, ResolvingMetadata }; |
framework/meta/nextjs/types/metadata-types.ts created+155| ... | ... | @@ -0,0 +1,155 @@ |
| 1 | /** | |
| 2 | * Metadata types | |
| 3 | */ | |
| 4 | ||
| 5 | export interface DeprecatedMetadataFields { | |
| 6 | /** | |
| 7 | * Deprecated options that have a preferred method | |
| 8 | * @deprecated Use appWebApp to configure apple-mobile-web-app-capable which provides | |
| 9 | * @see https://www.appsloveworld.com/coding/iphone/11/difference-between-apple-mobile-web-app-capable-and-apple-touch-fullscreen-ipho | |
| 10 | */ | |
| 11 | "apple-touch-fullscreen"?: never; | |
| 12 | ||
| 13 | /** | |
| 14 | * Obsolete since iOS 7. | |
| 15 | * @see https://web.dev/apple-touch-icon/ | |
| 16 | * @deprecated use icons.apple or instead | |
| 17 | */ | |
| 18 | "apple-touch-icon-precomposed"?: never; | |
| 19 | } | |
| 20 | ||
| 21 | export type TemplateString = | |
| 22 | | DefaultTemplateString | |
| 23 | | AbsoluteTemplateString | |
| 24 | | AbsoluteString; | |
| 25 | export type DefaultTemplateString = { | |
| 26 | default: string; | |
| 27 | template: string; | |
| 28 | }; | |
| 29 | export type AbsoluteTemplateString = { | |
| 30 | absolute: string; | |
| 31 | template: string | null; | |
| 32 | }; | |
| 33 | export type AbsoluteString = { | |
| 34 | absolute: string; | |
| 35 | }; | |
| 36 | ||
| 37 | export type Author = { | |
| 38 | // renders as <link rel="author"... | |
| 39 | url?: string | URL; | |
| 40 | // renders as <meta name="author"... | |
| 41 | name?: string; | |
| 42 | }; | |
| 43 | ||
| 44 | // does not include "unsafe-URL". to use this users should | |
| 45 | // use '"unsafe-URL" as ReferrerEnum' | |
| 46 | export type ReferrerEnum = | |
| 47 | | "no-referrer" | |
| 48 | | "origin" | |
| 49 | | "no-referrer-when-downgrade" | |
| 50 | | "origin-when-cross-origin" | |
| 51 | | "same-origin" | |
| 52 | | "strict-origin" | |
| 53 | | "strict-origin-when-cross-origin"; | |
| 54 | ||
| 55 | export type ColorSchemeEnum = | |
| 56 | | "normal" | |
| 57 | | "light" | |
| 58 | | "dark" | |
| 59 | | "light dark" | |
| 60 | | "dark light" | |
| 61 | | "only light"; | |
| 62 | ||
| 63 | type RobotsInfo = { | |
| 64 | // all and none will be inferred from index/follow boolean options | |
| 65 | index?: boolean; | |
| 66 | follow?: boolean; | |
| 67 | ||
| 68 | /** @deprecated set index to false instead */ | |
| 69 | noindex?: never; | |
| 70 | /** @deprecated set follow to false instead */ | |
| 71 | nofollow?: never; | |
| 72 | ||
| 73 | noarchive?: boolean; | |
| 74 | nosnippet?: boolean; | |
| 75 | noimageindex?: boolean; | |
| 76 | nocache?: boolean; | |
| 77 | notranslate?: boolean; | |
| 78 | indexifembedded?: boolean; | |
| 79 | nositelinkssearchbox?: boolean; | |
| 80 | unavailable_after?: string; | |
| 81 | "max-video-preview"?: number | string; | |
| 82 | "max-image-preview"?: "none" | "standard" | "large"; | |
| 83 | "max-snippet"?: number; | |
| 84 | }; | |
| 85 | export type Robots = RobotsInfo & { | |
| 86 | // if you want to specify an alternate robots just for google | |
| 87 | googleBot?: string | RobotsInfo; | |
| 88 | }; | |
| 89 | ||
| 90 | export type ResolvedRobots = { | |
| 91 | basic: string | null; | |
| 92 | googleBot: string | null; | |
| 93 | }; | |
| 94 | ||
| 95 | export type IconURL = string | URL; | |
| 96 | export type Icon = IconURL | IconDescriptor; | |
| 97 | export type IconDescriptor = { | |
| 98 | url: string | URL; | |
| 99 | type?: string; | |
| 100 | sizes?: string; | |
| 101 | /** defaults to rel="icon" unless superseded by Icons map */ | |
| 102 | rel?: string; | |
| 103 | media?: string; | |
| 104 | /** | |
| 105 | * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority | |
| 106 | */ | |
| 107 | fetchPriority?: "high" | "low" | "auto"; | |
| 108 | }; | |
| 109 | ||
| 110 | export type Icons = { | |
| 111 | /** rel="icon" */ | |
| 112 | icon?: Icon | Icon[]; | |
| 113 | /** rel="shortcut icon" */ | |
| 114 | shortcut?: Icon | Icon[]; | |
| 115 | /** | |
| 116 | * @see https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html | |
| 117 | * rel="apple-touch-icon" | |
| 118 | */ | |
| 119 | apple?: Icon | Icon[]; | |
| 120 | /** rel inferred from descriptor, defaults to "icon" */ | |
| 121 | other?: IconDescriptor | IconDescriptor[]; | |
| 122 | }; | |
| 123 | ||
| 124 | export type Verification = { | |
| 125 | google?: null | string | number | (string | number)[]; | |
| 126 | yahoo?: null | string | number | (string | number)[]; | |
| 127 | yandex?: null | string | number | (string | number)[]; | |
| 128 | me?: null | string | number | (string | number)[]; | |
| 129 | // if you ad-hoc additional verification | |
| 130 | other?: { | |
| 131 | [name: string]: string | number | (string | number)[]; | |
| 132 | }; | |
| 133 | }; | |
| 134 | ||
| 135 | export type ResolvedVerification = { | |
| 136 | google?: null | (string | number)[]; | |
| 137 | yahoo?: null | (string | number)[]; | |
| 138 | yandex?: null | (string | number)[]; | |
| 139 | me?: null | (string | number)[]; | |
| 140 | other?: { | |
| 141 | [name: string]: (string | number)[]; | |
| 142 | }; | |
| 143 | }; | |
| 144 | ||
| 145 | export type ResolvedIcons = { | |
| 146 | icon: IconDescriptor[]; | |
| 147 | apple: IconDescriptor[]; | |
| 148 | shortcut?: IconDescriptor[]; | |
| 149 | other?: IconDescriptor[]; | |
| 150 | }; | |
| 151 | ||
| 152 | export type ThemeColorDescriptor = { | |
| 153 | color: string; | |
| 154 | media?: string; | |
| 155 | }; |
framework/meta/nextjs/types/opengraph-types.ts created+267| ... | ... | @@ -0,0 +1,267 @@ |
| 1 | import type { AbsoluteTemplateString, TemplateString } from "./metadata-types"; | |
| 2 | ||
| 3 | export type OpenGraphType = | |
| 4 | | "article" | |
| 5 | | "book" | |
| 6 | | "music.song" | |
| 7 | | "music.album" | |
| 8 | | "music.playlist" | |
| 9 | | "music.radio_station" | |
| 10 | | "profile" | |
| 11 | | "website" | |
| 12 | | "video.tv_show" | |
| 13 | | "video.other" | |
| 14 | | "video.movie" | |
| 15 | | "video.episode"; | |
| 16 | ||
| 17 | export type OpenGraph = | |
| 18 | | OpenGraphWebsite | |
| 19 | | OpenGraphArticle | |
| 20 | | OpenGraphBook | |
| 21 | | OpenGraphProfile | |
| 22 | | OpenGraphMusicSong | |
| 23 | | OpenGraphMusicAlbum | |
| 24 | | OpenGraphMusicPlaylist | |
| 25 | | OpenGraphRadioStation | |
| 26 | | OpenGraphVideoMovie | |
| 27 | | OpenGraphVideoEpisode | |
| 28 | | OpenGraphVideoTVShow | |
| 29 | | OpenGraphVideoOther | |
| 30 | | OpenGraphMetadata; | |
| 31 | ||
| 32 | // update this type to reflect actual locales | |
| 33 | type Locale = string; | |
| 34 | ||
| 35 | type OpenGraphMetadata = { | |
| 36 | determiner?: "a" | "an" | "the" | "auto" | ""; | |
| 37 | title?: string | TemplateString; | |
| 38 | description?: string; | |
| 39 | emails?: string | Array<string>; | |
| 40 | phoneNumbers?: string | Array<string>; | |
| 41 | faxNumbers?: string | Array<string>; | |
| 42 | siteName?: string; | |
| 43 | locale?: Locale; | |
| 44 | alternateLocale?: Locale | Array<Locale>; | |
| 45 | images?: OGImage | Array<OGImage>; | |
| 46 | audio?: OGAudio | Array<OGAudio>; | |
| 47 | videos?: OGVideo | Array<OGVideo>; | |
| 48 | url?: string | URL; | |
| 49 | countryName?: string; | |
| 50 | ttl?: number; | |
| 51 | }; | |
| 52 | type OpenGraphWebsite = OpenGraphMetadata & { | |
| 53 | type: "website"; | |
| 54 | }; | |
| 55 | type OpenGraphArticle = OpenGraphMetadata & { | |
| 56 | type: "article"; | |
| 57 | publishedTime?: string; // datetime | |
| 58 | modifiedTime?: string; // datetime | |
| 59 | expirationTime?: string; // datetime | |
| 60 | authors?: null | string | URL | Array<string | URL>; | |
| 61 | section?: null | string; | |
| 62 | tags?: null | string | Array<string>; | |
| 63 | }; | |
| 64 | type OpenGraphBook = OpenGraphMetadata & { | |
| 65 | type: "book"; | |
| 66 | isbn?: null | string; | |
| 67 | releaseDate?: null | string; // datetime | |
| 68 | authors?: null | string | URL | Array<string | URL>; | |
| 69 | tags?: null | string | Array<string>; | |
| 70 | }; | |
| 71 | type OpenGraphProfile = OpenGraphMetadata & { | |
| 72 | type: "profile"; | |
| 73 | firstName?: null | string; | |
| 74 | lastName?: null | string; | |
| 75 | username?: null | string; | |
| 76 | gender?: null | string; | |
| 77 | }; | |
| 78 | type OpenGraphMusicSong = OpenGraphMetadata & { | |
| 79 | type: "music.song"; | |
| 80 | duration?: null | number; | |
| 81 | albums?: null | string | URL | OGAlbum | Array<string | URL | OGAlbum>; | |
| 82 | musicians?: null | string | URL | Array<string | URL>; | |
| 83 | }; | |
| 84 | type OpenGraphMusicAlbum = OpenGraphMetadata & { | |
| 85 | type: "music.album"; | |
| 86 | songs?: null | string | URL | OGSong | Array<string | URL | OGSong>; | |
| 87 | musicians?: null | string | URL | Array<string | URL>; | |
| 88 | releaseDate?: null | string; // datetime | |
| 89 | }; | |
| 90 | type OpenGraphMusicPlaylist = OpenGraphMetadata & { | |
| 91 | type: "music.playlist"; | |
| 92 | songs?: null | string | URL | OGSong | Array<string | URL | OGSong>; | |
| 93 | creators?: null | string | URL | Array<string | URL>; | |
| 94 | }; | |
| 95 | type OpenGraphRadioStation = OpenGraphMetadata & { | |
| 96 | type: "music.radio_station"; | |
| 97 | creators?: null | string | URL | Array<string | URL>; | |
| 98 | }; | |
| 99 | type OpenGraphVideoMovie = OpenGraphMetadata & { | |
| 100 | type: "video.movie"; | |
| 101 | actors?: null | string | URL | OGActor | Array<string | URL | OGActor>; | |
| 102 | directors?: null | string | URL | Array<string | URL>; | |
| 103 | writers?: null | string | URL | Array<string | URL>; | |
| 104 | duration?: null | number; | |
| 105 | releaseDate?: null | string; // datetime | |
| 106 | tags?: null | string | Array<string>; | |
| 107 | }; | |
| 108 | type OpenGraphVideoEpisode = OpenGraphMetadata & { | |
| 109 | type: "video.episode"; | |
| 110 | actors?: null | string | URL | OGActor | Array<string | URL | OGActor>; | |
| 111 | directors?: null | string | URL | Array<string | URL>; | |
| 112 | writers?: null | string | URL | Array<string | URL>; | |
| 113 | duration?: null | number; | |
| 114 | releaseDate?: null | string; // datetime | |
| 115 | tags?: null | string | Array<string>; | |
| 116 | series?: null | string | URL; | |
| 117 | }; | |
| 118 | type OpenGraphVideoTVShow = OpenGraphMetadata & { | |
| 119 | type: "video.tv_show"; | |
| 120 | }; | |
| 121 | type OpenGraphVideoOther = OpenGraphMetadata & { | |
| 122 | type: "video.other"; | |
| 123 | }; | |
| 124 | ||
| 125 | type OGImage = string | OGImageDescriptor | URL; | |
| 126 | type OGImageDescriptor = { | |
| 127 | url: string | URL; | |
| 128 | secureUrl?: string | URL; | |
| 129 | alt?: string; | |
| 130 | type?: string; | |
| 131 | width?: string | number; | |
| 132 | height?: string | number; | |
| 133 | }; | |
| 134 | type OGAudio = string | OGAudioDescriptor | URL; | |
| 135 | type OGAudioDescriptor = { | |
| 136 | url: string | URL; | |
| 137 | secureUrl?: string | URL; | |
| 138 | type?: string; | |
| 139 | }; | |
| 140 | type OGVideo = string | OGVideoDescriptor | URL; | |
| 141 | type OGVideoDescriptor = { | |
| 142 | url: string | URL; | |
| 143 | secureUrl?: string | URL; | |
| 144 | type?: string; | |
| 145 | width?: string | number; | |
| 146 | height?: string | number; | |
| 147 | }; | |
| 148 | ||
| 149 | export type ResolvedOpenGraph = | |
| 150 | | ResolvedOpenGraphWebsite | |
| 151 | | ResolvedOpenGraphArticle | |
| 152 | | ResolvedOpenGraphBook | |
| 153 | | ResolvedOpenGraphProfile | |
| 154 | | ResolvedOpenGraphMusicSong | |
| 155 | | ResolvedOpenGraphMusicAlbum | |
| 156 | | ResolvedOpenGraphMusicPlaylist | |
| 157 | | ResolvedOpenGraphRadioStation | |
| 158 | | ResolvedOpenGraphVideoMovie | |
| 159 | | ResolvedOpenGraphVideoEpisode | |
| 160 | | ResolvedOpenGraphVideoTVShow | |
| 161 | | ResolvedOpenGraphVideoOther | |
| 162 | | ResolvedOpenGraphMetadata; | |
| 163 | ||
| 164 | type ResolvedOpenGraphMetadata = { | |
| 165 | determiner?: "a" | "an" | "the" | "auto" | ""; | |
| 166 | title?: AbsoluteTemplateString; | |
| 167 | description?: string; | |
| 168 | emails?: Array<string>; | |
| 169 | phoneNumbers?: Array<string>; | |
| 170 | faxNumbers?: Array<string>; | |
| 171 | siteName?: string; | |
| 172 | locale?: Locale; | |
| 173 | alternateLocale?: Array<Locale>; | |
| 174 | images?: Array<OGImage>; | |
| 175 | audio?: Array<OGAudio>; | |
| 176 | videos?: Array<OGVideo>; | |
| 177 | url: null | URL | string; | |
| 178 | countryName?: string; | |
| 179 | ttl?: number; | |
| 180 | }; | |
| 181 | type ResolvedOpenGraphWebsite = ResolvedOpenGraphMetadata & { | |
| 182 | type: "website"; | |
| 183 | }; | |
| 184 | type ResolvedOpenGraphArticle = ResolvedOpenGraphMetadata & { | |
| 185 | type: "article"; | |
| 186 | publishedTime?: string; // datetime | |
| 187 | modifiedTime?: string; // datetime | |
| 188 | expirationTime?: string; // datetime | |
| 189 | authors?: Array<string>; | |
| 190 | section?: string; | |
| 191 | tags?: Array<string>; | |
| 192 | }; | |
| 193 | type ResolvedOpenGraphBook = ResolvedOpenGraphMetadata & { | |
| 194 | type: "book"; | |
| 195 | isbn?: string; | |
| 196 | releaseDate?: string; // datetime | |
| 197 | authors?: Array<string>; | |
| 198 | tags?: Array<string>; | |
| 199 | }; | |
| 200 | type ResolvedOpenGraphProfile = ResolvedOpenGraphMetadata & { | |
| 201 | type: "profile"; | |
| 202 | firstName?: string; | |
| 203 | lastName?: string; | |
| 204 | username?: string; | |
| 205 | gender?: string; | |
| 206 | }; | |
| 207 | type ResolvedOpenGraphMusicSong = ResolvedOpenGraphMetadata & { | |
| 208 | type: "music.song"; | |
| 209 | duration?: number; | |
| 210 | albums?: Array<OGAlbum>; | |
| 211 | musicians?: Array<string | URL>; | |
| 212 | }; | |
| 213 | type ResolvedOpenGraphMusicAlbum = ResolvedOpenGraphMetadata & { | |
| 214 | type: "music.album"; | |
| 215 | songs?: Array<string | URL | OGSong>; | |
| 216 | musicians?: Array<string | URL>; | |
| 217 | releaseDate?: string; // datetime | |
| 218 | }; | |
| 219 | type ResolvedOpenGraphMusicPlaylist = ResolvedOpenGraphMetadata & { | |
| 220 | type: "music.playlist"; | |
| 221 | songs?: Array<string | URL | OGSong>; | |
| 222 | creators?: Array<string | URL>; | |
| 223 | }; | |
| 224 | type ResolvedOpenGraphRadioStation = ResolvedOpenGraphMetadata & { | |
| 225 | type: "music.radio_station"; | |
| 226 | creators?: Array<string | URL>; | |
| 227 | }; | |
| 228 | type ResolvedOpenGraphVideoMovie = ResolvedOpenGraphMetadata & { | |
| 229 | type: "video.movie"; | |
| 230 | actors?: Array<string | URL | OGActor>; | |
| 231 | directors?: Array<string | URL>; | |
| 232 | writers?: Array<string | URL>; | |
| 233 | duration?: number; | |
| 234 | releaseDate?: string; // datetime | |
| 235 | tags?: Array<string>; | |
| 236 | }; | |
| 237 | type ResolvedOpenGraphVideoEpisode = ResolvedOpenGraphMetadata & { | |
| 238 | type: "video.episode"; | |
| 239 | actors?: Array<string | URL | OGActor>; | |
| 240 | directors?: Array<string | URL>; | |
| 241 | writers?: Array<string | URL>; | |
| 242 | duration?: number; | |
| 243 | releaseDate?: string; // datetime | |
| 244 | tags?: Array<string>; | |
| 245 | series?: string | URL; | |
| 246 | }; | |
| 247 | type ResolvedOpenGraphVideoTVShow = ResolvedOpenGraphMetadata & { | |
| 248 | type: "video.tv_show"; | |
| 249 | }; | |
| 250 | type ResolvedOpenGraphVideoOther = ResolvedOpenGraphMetadata & { | |
| 251 | type: "video.other"; | |
| 252 | }; | |
| 253 | ||
| 254 | type OGSong = { | |
| 255 | url: string | URL; | |
| 256 | disc?: number; | |
| 257 | track?: number; | |
| 258 | }; | |
| 259 | type OGAlbum = { | |
| 260 | url: string | URL; | |
| 261 | disc?: number; | |
| 262 | track?: number; | |
| 263 | }; | |
| 264 | type OGActor = { | |
| 265 | url: string | URL; | |
| 266 | role?: string; | |
| 267 | }; |
framework/meta/nextjs/types/resolvers.ts created+17| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | import { Metadata, ResolvedMetadata } from "./metadata-interface"; | |
| 2 | ||
| 3 | export type FieldResolver<Key extends keyof Metadata> = ( | |
| 4 | T: Metadata[Key], | |
| 5 | ) => ResolvedMetadata[Key]; | |
| 6 | export type FieldResolverWithMetadataBase< | |
| 7 | Key extends keyof Metadata, | |
| 8 | Options = undefined, | |
| 9 | > = Options extends undefined ? ( | |
| 10 | T: Metadata[Key], | |
| 11 | metadataBase: ResolvedMetadata["metadataBase"], | |
| 12 | ) => ResolvedMetadata[Key] | |
| 13 | : ( | |
| 14 | T: Metadata[Key], | |
| 15 | metadataBase: ResolvedMetadata["metadataBase"], | |
| 16 | options: Options, | |
| 17 | ) => ResolvedMetadata[Key]; |
framework/meta/nextjs/types/twitter-types.ts created+94| ... | ... | @@ -0,0 +1,94 @@ |
| 1 | // Reference: https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup | |
| 2 | ||
| 3 | import type { AbsoluteTemplateString, TemplateString } from "./metadata-types"; | |
| 4 | ||
| 5 | export type Twitter = | |
| 6 | | TwitterSummary | |
| 7 | | TwitterSummaryLargeImage | |
| 8 | | TwitterPlayer | |
| 9 | | TwitterApp | |
| 10 | | TwitterMetadata; | |
| 11 | ||
| 12 | type TwitterMetadata = { | |
| 13 | // defaults to card="summary" | |
| 14 | site?: string; // username for account associated to the site itself | |
| 15 | siteId?: string; // id for account associated to the site itself | |
| 16 | creator?: string; // username for the account associated to the creator of the content on the site | |
| 17 | creatorId?: string; // id for the account associated to the creator of the content on the site | |
| 18 | description?: string; | |
| 19 | title?: string | TemplateString; | |
| 20 | images?: TwitterImage | Array<TwitterImage>; | |
| 21 | }; | |
| 22 | type TwitterSummary = TwitterMetadata & { | |
| 23 | card: "summary"; | |
| 24 | }; | |
| 25 | type TwitterSummaryLargeImage = TwitterMetadata & { | |
| 26 | card: "summary_large_image"; | |
| 27 | }; | |
| 28 | type TwitterPlayer = TwitterMetadata & { | |
| 29 | card: "player"; | |
| 30 | players: TwitterPlayerDescriptor | Array<TwitterPlayerDescriptor>; | |
| 31 | }; | |
| 32 | type TwitterApp = TwitterMetadata & { | |
| 33 | card: "app"; | |
| 34 | app: TwitterAppDescriptor; | |
| 35 | }; | |
| 36 | export type TwitterAppDescriptor = { | |
| 37 | id: { | |
| 38 | iphone?: string | number; | |
| 39 | ipad?: string | number; | |
| 40 | googleplay?: string; | |
| 41 | }; | |
| 42 | url?: { | |
| 43 | iphone?: string | URL; | |
| 44 | ipad?: string | URL; | |
| 45 | googleplay?: string | URL; | |
| 46 | }; | |
| 47 | name?: string; | |
| 48 | }; | |
| 49 | ||
| 50 | type TwitterImage = string | TwitterImageDescriptor | URL; | |
| 51 | type TwitterImageDescriptor = { | |
| 52 | url: string | URL; | |
| 53 | alt?: string; | |
| 54 | secureUrl?: string | URL; | |
| 55 | type?: string; | |
| 56 | width?: string | number; | |
| 57 | height?: string | number; | |
| 58 | }; | |
| 59 | type TwitterPlayerDescriptor = { | |
| 60 | playerUrl: string | URL; | |
| 61 | streamUrl: string | URL; | |
| 62 | width: number; | |
| 63 | height: number; | |
| 64 | }; | |
| 65 | ||
| 66 | type ResolvedTwitterImage = { | |
| 67 | url: string | URL; | |
| 68 | alt?: string; | |
| 69 | secureUrl?: string | URL; | |
| 70 | type?: string; | |
| 71 | width?: string | number; | |
| 72 | height?: string | number; | |
| 73 | }; | |
| 74 | type ResolvedTwitterSummary = { | |
| 75 | site: string | null; | |
| 76 | siteId: string | null; | |
| 77 | creator: string | null; | |
| 78 | creatorId: string | null; | |
| 79 | description: string | null; | |
| 80 | title: AbsoluteTemplateString; | |
| 81 | images?: Array<ResolvedTwitterImage>; | |
| 82 | }; | |
| 83 | type ResolvedTwitterPlayer = ResolvedTwitterSummary & { | |
| 84 | players: Array<TwitterPlayerDescriptor>; | |
| 85 | }; | |
| 86 | type ResolvedTwitterApp = ResolvedTwitterSummary & { | |
| 87 | app: TwitterAppDescriptor; | |
| 88 | }; | |
| 89 | ||
| 90 | export type ResolvedTwitterMetadata = | |
| 91 | | ({ card: "summary" } & ResolvedTwitterSummary) | |
| 92 | | ({ card: "summary_large_image" } & ResolvedTwitterSummary) | |
| 93 | | ({ card: "player" } & ResolvedTwitterPlayer) | |
| 94 | | ({ card: "app" } & ResolvedTwitterApp); |
framework/meta/readme.md created+5| ... | ... | @@ -0,0 +1,5 @@ |
| 1 | the metadata renderer was written in 2022, and is 3700 lines of code. it | |
| 2 | represents the bulk of the code in the framework, which i think is wrong. | |
| 3 | ||
| 4 | a bounty goes to rewriting this codebase into one or two files. merging logic is | |
| 5 | surely not needed, and resolution can happen in the same step as rendering. |
framework/meta/render.ts created+565| ... | ... | @@ -0,0 +1,565 @@ |
| 1 | import type { Icon, ResolvedMetadata } from "./types"; | |
| 2 | import { escapeHTML as esc } from "./utils"; | |
| 3 | ||
| 4 | function Meta(name: string, content: any) { | |
| 5 | return `<meta name="${esc(name)}" content="${esc(content)}">`; | |
| 6 | } | |
| 7 | ||
| 8 | function MetaProp(name: string, content: any) { | |
| 9 | return `<meta property="${esc(name)}" content="${esc(content)}">`; | |
| 10 | } | |
| 11 | ||
| 12 | function MetaMedia(name: string, content: any, media: string) { | |
| 13 | return `<meta name="${esc(name)}" content="${esc(content)}" media="${ | |
| 14 | esc(media) | |
| 15 | }">`; | |
| 16 | } | |
| 17 | ||
| 18 | function Link(rel: string, href: any) { | |
| 19 | return `<link rel="${esc(rel)}" href="${esc(href)}" />`; | |
| 20 | } | |
| 21 | ||
| 22 | function LinkMedia(rel: string, href: any, media: string) { | |
| 23 | return `<link rel="${esc(rel)}" href="${esc(href)}" media="${esc(media)}">`; | |
| 24 | } | |
| 25 | ||
| 26 | const resolveUrl = ( | |
| 27 | url: string | URL, | |
| 28 | ) => (typeof url === "string" ? url : url.toString()); | |
| 29 | ||
| 30 | function IconLink(rel: string, icon: Icon) { | |
| 31 | if (typeof icon === "object" && !(icon instanceof URL)) { | |
| 32 | const { url, rel: _, ...props } = icon; | |
| 33 | return `<link rel="${esc(rel)}" href="${esc(resolveUrl(url))}"${ | |
| 34 | Object.keys(props) | |
| 35 | .map((key) => ` ${key}="${esc(props[key])}"`) | |
| 36 | .join("") | |
| 37 | }>`; | |
| 38 | } else { | |
| 39 | const href = resolveUrl(icon); | |
| 40 | return Link(rel, href); | |
| 41 | } | |
| 42 | } | |
| 43 | ||
| 44 | function ExtendMeta(prefix: string, content: any) { | |
| 45 | if ( | |
| 46 | typeof content === "string" || typeof content === "number" || | |
| 47 | content instanceof URL | |
| 48 | ) { | |
| 49 | return MetaProp(prefix, content); | |
| 50 | } else { | |
| 51 | let str = ""; | |
| 52 | for (const [prop, value] of Object.entries(content)) { | |
| 53 | if (value) { | |
| 54 | str += MetaProp( | |
| 55 | prefix === "og:image" && prop === "url" | |
| 56 | ? "og:image" | |
| 57 | : prefix + ":" + prop, | |
| 58 | value, | |
| 59 | ); | |
| 60 | } | |
| 61 | } | |
| 62 | return str; | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | const formatDetectionKeys = [ | |
| 67 | "telephone", | |
| 68 | "date", | |
| 69 | "address", | |
| 70 | "email", | |
| 71 | "url", | |
| 72 | ] as const; | |
| 73 | ||
| 74 | export function renderMetadata(meta: ResolvedMetadata): string { | |
| 75 | var str = ""; | |
| 76 | ||
| 77 | // <BasicMetadata/> | |
| 78 | if (meta.title?.absolute) str += `<title>${esc(meta.title.absolute)}</title>`; | |
| 79 | if (meta.description) str += Meta("description", meta.description); | |
| 80 | if (meta.applicationName) { | |
| 81 | str += Meta("application-name", meta.applicationName); | |
| 82 | } | |
| 83 | if (meta.authors) { | |
| 84 | for (var author of meta.authors) { | |
| 85 | if (author.url) str += Link("author", author.url); | |
| 86 | if (author.name) str += Meta("author", author.name); | |
| 87 | } | |
| 88 | } | |
| 89 | if (meta.manifest) str += Link("manifest", meta.manifest); | |
| 90 | if (meta.generator) str += Meta("generator", meta.generator); | |
| 91 | if (meta.referrer) str += Meta("referrer", meta.referrer); | |
| 92 | if (meta.themeColor) { | |
| 93 | for (var themeColor of meta.themeColor) { | |
| 94 | str += !themeColor.media | |
| 95 | ? Meta("theme-color", themeColor.color) | |
| 96 | : MetaMedia("theme-color", themeColor.color, themeColor.media); | |
| 97 | } | |
| 98 | } | |
| 99 | if (meta.colorScheme) str += Meta("color-scheme", meta.colorScheme); | |
| 100 | if (meta.viewport) str += Meta("viewport", meta.viewport); | |
| 101 | if (meta.creator) str += Meta("creator", meta.creator); | |
| 102 | if (meta.publisher) str += Meta("publisher", meta.publisher); | |
| 103 | if (meta.robots?.basic) str += Meta("robots", meta.robots.basic); | |
| 104 | if (meta.robots?.googleBot) str += Meta("googlebot", meta.robots.googleBot); | |
| 105 | if (meta.abstract) str += Meta("abstract", meta.abstract); | |
| 106 | if (meta.archives) { | |
| 107 | for (var archive of meta.archives) { | |
| 108 | str += Link("archives", archive); | |
| 109 | } | |
| 110 | } | |
| 111 | if (meta.assets) { | |
| 112 | for (var asset of meta.assets) { | |
| 113 | str += Link("assets", asset); | |
| 114 | } | |
| 115 | } | |
| 116 | if (meta.bookmarks) { | |
| 117 | for (var bookmark of meta.bookmarks) { | |
| 118 | str += Link("bookmarks", bookmark); | |
| 119 | } | |
| 120 | } | |
| 121 | if (meta.category) str += Meta("category", meta.category); | |
| 122 | if (meta.classification) str += Meta("classification", meta.classification); | |
| 123 | if (meta.other) { | |
| 124 | for (var [name, content] of Object.entries(meta.other)) { | |
| 125 | if (content) { | |
| 126 | str += Meta(name, Array.isArray(content) ? content.join(",") : content); | |
| 127 | } | |
| 128 | } | |
| 129 | } | |
| 130 | ||
| 131 | // <AlternatesMetadata /> | |
| 132 | var alternates = meta.alternates; | |
| 133 | if (alternates) { | |
| 134 | if (alternates.canonical) { | |
| 135 | str += Link("canonical", alternates.canonical.url); | |
| 136 | } | |
| 137 | if (alternates.languages) { | |
| 138 | for (var [locale, urls] of Object.entries(alternates.languages)) { | |
| 139 | for (var { url, title } of urls) { | |
| 140 | str += `<link rel="alternate" hreflang="${esc(locale)}" href="${ | |
| 141 | esc(url.toString()) | |
| 142 | }"${title ? ` title="${esc(title)}"` : ""}>`; | |
| 143 | } | |
| 144 | } | |
| 145 | } | |
| 146 | if (alternates.media) { | |
| 147 | for (var [media, urls2] of Object.entries(alternates.media)) { | |
| 148 | if (urls2) { | |
| 149 | for (var { url, title } of urls2) { | |
| 150 | str += `<link rel="alternate" media="${esc(media)}" href="${ | |
| 151 | esc(url.toString()) | |
| 152 | }"${title ? ` title="${esc(title)}"` : ""}>`; | |
| 153 | } | |
| 154 | } | |
| 155 | } | |
| 156 | } | |
| 157 | if (alternates.types) { | |
| 158 | for (var [type, urls2] of Object.entries(alternates.types)) { | |
| 159 | if (urls2) { | |
| 160 | for (var { url, title } of urls2) { | |
| 161 | str += `<link rel="alternate" type="${esc(type)}" href="${ | |
| 162 | esc(url.toString()) | |
| 163 | }"${title ? ` title="${esc(title)}"` : ""}>`; | |
| 164 | } | |
| 165 | } | |
| 166 | } | |
| 167 | } | |
| 168 | } | |
| 169 | ||
| 170 | // <ItunesMeta /> | |
| 171 | if (meta.itunes) { | |
| 172 | str += Meta( | |
| 173 | "apple-itunes-app", | |
| 174 | `app-id=${meta.itunes.appId}${ | |
| 175 | meta.itunes.appArgument | |
| 176 | ? `, app-argument=${meta.itunes.appArgument}` | |
| 177 | : "" | |
| 178 | }`, | |
| 179 | ); | |
| 180 | } | |
| 181 | ||
| 182 | // <FormatDetectionMeta /> | |
| 183 | if (meta.formatDetection) { | |
| 184 | var contentStr = ""; | |
| 185 | for (var key of formatDetectionKeys) { | |
| 186 | if (key in meta.formatDetection) { | |
| 187 | if (contentStr) contentStr += ", "; | |
| 188 | contentStr += `${key}=no`; | |
| 189 | } | |
| 190 | } | |
| 191 | str += Meta("format-detection", contentStr); | |
| 192 | } | |
| 193 | ||
| 194 | // <VerificationMeta /> | |
| 195 | if (meta.verification) { | |
| 196 | if (meta.verification.google) { | |
| 197 | for (var verificationKey of meta.verification.google) { | |
| 198 | str += Meta("google-site-verification", verificationKey); | |
| 199 | } | |
| 200 | } | |
| 201 | if (meta.verification.yahoo) { | |
| 202 | for (var verificationKey of meta.verification.yahoo) { | |
| 203 | str += Meta("y_key", verificationKey); | |
| 204 | } | |
| 205 | } | |
| 206 | if (meta.verification.yandex) { | |
| 207 | for (var verificationKey of meta.verification.yandex) { | |
| 208 | str += Meta("yandex-verification", verificationKey); | |
| 209 | } | |
| 210 | } | |
| 211 | if (meta.verification.me) { | |
| 212 | for (var verificationKey of meta.verification.me) { | |
| 213 | str += Meta("me", verificationKey); | |
| 214 | } | |
| 215 | } | |
| 216 | if (meta.verification.other) { | |
| 217 | for ( | |
| 218 | var [verificationKey2, values] of Object.entries( | |
| 219 | meta.verification.other, | |
| 220 | ) | |
| 221 | ) { | |
| 222 | for (var value of values) { | |
| 223 | str += Meta(verificationKey2, value); | |
| 224 | } | |
| 225 | } | |
| 226 | } | |
| 227 | } | |
| 228 | ||
| 229 | // <AppleWebAppMeta /> | |
| 230 | if (meta.appleWebApp) { | |
| 231 | const { capable, title, startupImage, statusBarStyle } = meta.appleWebApp; | |
| 232 | if (capable) { | |
| 233 | str += '<meta name="apple-mobile-web-app-capable" content="yes" />'; | |
| 234 | } | |
| 235 | if (title) str += Meta("apple-mobile-web-app-title", title); | |
| 236 | if (startupImage) { | |
| 237 | for (const img of startupImage) { | |
| 238 | str += !img.media | |
| 239 | ? Link("apple-touch-startup-image", img.url) | |
| 240 | : LinkMedia("apple-touch-startup-image", img.url, img.media); | |
| 241 | } | |
| 242 | } | |
| 243 | if (statusBarStyle) { | |
| 244 | str += Meta("apple-mobile-web-app-status-bar-style", statusBarStyle); | |
| 245 | } | |
| 246 | } | |
| 247 | ||
| 248 | // <OpenGraphMetadata /> | |
| 249 | if (meta.openGraph) { | |
| 250 | const og = meta.openGraph; | |
| 251 | if (og.determiner) str += MetaProp("og:determiner", og.determiner); | |
| 252 | if (og.title?.absolute) str += MetaProp("og:title", og.title.absolute); | |
| 253 | if (og.description) str += MetaProp("og:description", og.description); | |
| 254 | if (og.url) str += MetaProp("og:url", og.url.toString()); | |
| 255 | if (og.siteName) str += MetaProp("og:site_name", og.siteName); | |
| 256 | if (og.locale) str += MetaProp("og:locale", og.locale); | |
| 257 | if (og.countryName) str += MetaProp("og:country_name", og.countryName); | |
| 258 | if (og.ttl) str += MetaProp("og:ttl", og.ttl); | |
| 259 | if (og.images) { | |
| 260 | for (const item of og.images) { | |
| 261 | str += ExtendMeta("og:image", item); | |
| 262 | } | |
| 263 | } | |
| 264 | if (og.videos) { | |
| 265 | for (const item of og.videos) { | |
| 266 | str += ExtendMeta("og:video", item); | |
| 267 | } | |
| 268 | } | |
| 269 | if (og.audio) { | |
| 270 | for (const item of og.audio) { | |
| 271 | str += ExtendMeta("og:audio", item); | |
| 272 | } | |
| 273 | } | |
| 274 | if (og.emails) { | |
| 275 | for (const item of og.emails) { | |
| 276 | str += ExtendMeta("og:email", item); | |
| 277 | } | |
| 278 | } | |
| 279 | if (og.phoneNumbers) { | |
| 280 | for (const item of og.phoneNumbers) { | |
| 281 | str += MetaProp("og:phone_number", item); | |
| 282 | } | |
| 283 | } | |
| 284 | if (og.faxNumbers) { | |
| 285 | for (const item of og.faxNumbers) { | |
| 286 | str += MetaProp("og:fax_number", item); | |
| 287 | } | |
| 288 | } | |
| 289 | if (og.alternateLocale) { | |
| 290 | for (const item of og.alternateLocale) { | |
| 291 | str += MetaProp("og:locale:alternate", item); | |
| 292 | } | |
| 293 | } | |
| 294 | ||
| 295 | if ("type" in og) { | |
| 296 | str += MetaProp("og:type", og.type); | |
| 297 | switch (og.type) { | |
| 298 | case "website": | |
| 299 | break; | |
| 300 | case "article": | |
| 301 | if (og.publishedTime) { | |
| 302 | str += MetaProp("article:published_time", og.publishedTime); | |
| 303 | } | |
| 304 | if (og.modifiedTime) { | |
| 305 | str += MetaProp("article:modified_time", og.modifiedTime); | |
| 306 | } | |
| 307 | if (og.expirationTime) { | |
| 308 | str += MetaProp("article:expiration_time", og.expirationTime); | |
| 309 | } | |
| 310 | if (og.authors) { | |
| 311 | for (const item of og.authors) { | |
| 312 | str += MetaProp("article:author", item); | |
| 313 | } | |
| 314 | } | |
| 315 | if (og.section) str += MetaProp("article:section", og.section); | |
| 316 | if (og.tags) { | |
| 317 | for (const item of og.tags) { | |
| 318 | str += MetaProp("article:tag", item); | |
| 319 | } | |
| 320 | } | |
| 321 | break; | |
| 322 | case "book": | |
| 323 | if (og.isbn) str += MetaProp("book:isbn", og.isbn); | |
| 324 | if (og.releaseDate) { | |
| 325 | str += MetaProp("book:release_date", og.releaseDate); | |
| 326 | } | |
| 327 | if (og.authors) { | |
| 328 | for (const item of og.authors) { | |
| 329 | str += MetaProp("article:author", item); | |
| 330 | } | |
| 331 | } | |
| 332 | if (og.tags) { | |
| 333 | for (const item of og.tags) { | |
| 334 | str += MetaProp("article:tag", item); | |
| 335 | } | |
| 336 | } | |
| 337 | break; | |
| 338 | case "profile": | |
| 339 | if (og.firstName) str += MetaProp("profile:first_name", og.firstName); | |
| 340 | if (og.lastName) str += MetaProp("profile:last_name", og.lastName); | |
| 341 | if (og.username) str += MetaProp("profile:first_name", og.username); | |
| 342 | if (og.gender) str += MetaProp("profile:first_name", og.gender); | |
| 343 | break; | |
| 344 | case "music.song": | |
| 345 | if (og.duration) str += MetaProp("music:duration", og.duration); | |
| 346 | if (og.albums) { | |
| 347 | for (const item of og.albums) { | |
| 348 | str += ExtendMeta("music:albums", item); | |
| 349 | } | |
| 350 | } | |
| 351 | if (og.musicians) { | |
| 352 | for (const item of og.musicians) { | |
| 353 | str += MetaProp("music:musician", item); | |
| 354 | } | |
| 355 | } | |
| 356 | break; | |
| 357 | case "music.album": | |
| 358 | if (og.songs) { | |
| 359 | for (const item of og.songs) { | |
| 360 | str += ExtendMeta("music:song", item); | |
| 361 | } | |
| 362 | } | |
| 363 | if (og.musicians) { | |
| 364 | for (const item of og.musicians) { | |
| 365 | str += MetaProp("music:musician", item); | |
| 366 | } | |
| 367 | } | |
| 368 | if (og.releaseDate) { | |
| 369 | str += MetaProp("music:release_date", og.releaseDate); | |
| 370 | } | |
| 371 | break; | |
| 372 | case "music.playlist": | |
| 373 | if (og.songs) { | |
| 374 | for (const item of og.songs) { | |
| 375 | str += ExtendMeta("music:song", item); | |
| 376 | } | |
| 377 | } | |
| 378 | if (og.creators) { | |
| 379 | for (const item of og.creators) { | |
| 380 | str += MetaProp("music:creator", item); | |
| 381 | } | |
| 382 | } | |
| 383 | break; | |
| 384 | case "music.radio_station": | |
| 385 | if (og.creators) { | |
| 386 | for (const item of og.creators) { | |
| 387 | str += MetaProp("music:creator", item); | |
| 388 | } | |
| 389 | } | |
| 390 | break; | |
| 391 | case "video.movie": | |
| 392 | if (og.actors) { | |
| 393 | for (const item of og.actors) { | |
| 394 | str += ExtendMeta("video:actor", item); | |
| 395 | } | |
| 396 | } | |
| 397 | if (og.directors) { | |
| 398 | for (const item of og.directors) { | |
| 399 | str += MetaProp("video:director", item); | |
| 400 | } | |
| 401 | } | |
| 402 | if (og.writers) { | |
| 403 | for (const item of og.writers) { | |
| 404 | str += MetaProp("video:writer", item); | |
| 405 | } | |
| 406 | } | |
| 407 | if (og.duration) str += MetaProp("video:duration", og.duration); | |
| 408 | if (og.releaseDate) { | |
| 409 | str += MetaProp("video:release_date", og.releaseDate); | |
| 410 | } | |
| 411 | if (og.tags) { | |
| 412 | for (const item of og.tags) { | |
| 413 | str += MetaProp("video:tag", item); | |
| 414 | } | |
| 415 | } | |
| 416 | break; | |
| 417 | case "video.episode": | |
| 418 | if (og.actors) { | |
| 419 | for (const item of og.actors) { | |
| 420 | str += ExtendMeta("video:actor", item); | |
| 421 | } | |
| 422 | } | |
| 423 | if (og.directors) { | |
| 424 | for (const item of og.directors) { | |
| 425 | str += MetaProp("video:director", item); | |
| 426 | } | |
| 427 | } | |
| 428 | if (og.writers) { | |
| 429 | for (const item of og.writers) { | |
| 430 | str += MetaProp("video:writer", item); | |
| 431 | } | |
| 432 | } | |
| 433 | if (og.duration) str += MetaProp("video:duration", og.duration); | |
| 434 | if (og.releaseDate) { | |
| 435 | str += MetaProp("video:release_date", og.releaseDate); | |
| 436 | } | |
| 437 | if (og.tags) { | |
| 438 | for (const item of og.tags) { | |
| 439 | str += MetaProp("video:tag", item); | |
| 440 | } | |
| 441 | } | |
| 442 | if (og.series) str += MetaProp("video:series", og.series); | |
| 443 | break; | |
| 444 | case "video.other": | |
| 445 | case "video.tv_show": | |
| 446 | default: | |
| 447 | throw new Error("Invalid OpenGraph type: " + og.type); | |
| 448 | } | |
| 449 | } | |
| 450 | } | |
| 451 | ||
| 452 | // <TwitterMetadata /> | |
| 453 | if (meta.twitter) { | |
| 454 | const twitter = meta.twitter; | |
| 455 | ||
| 456 | if (twitter.card) str += Meta("twitter:card", twitter.card); | |
| 457 | if (twitter.site) str += Meta("twitter:site", twitter.site); | |
| 458 | if (twitter.siteId) str += Meta("twitter:site:id", twitter.siteId); | |
| 459 | if (twitter.creator) str += Meta("twitter:creator", twitter.creator); | |
| 460 | if (twitter.creatorId) str += Meta("twitter:creator:id", twitter.creatorId); | |
| 461 | if (twitter.title?.absolute) { | |
| 462 | str += Meta("twitter:title", twitter.title.absolute); | |
| 463 | } | |
| 464 | if (twitter.description) { | |
| 465 | str += Meta("twitter:description", twitter.description); | |
| 466 | } | |
| 467 | if (twitter.images) { | |
| 468 | for (const img of twitter.images) { | |
| 469 | str += Meta("twitter:image", img.url); | |
| 470 | if (img.alt) str += Meta("twitter:image:alt", img.alt); | |
| 471 | } | |
| 472 | } | |
| 473 | if (twitter.card === "player") { | |
| 474 | for (const player of twitter.players) { | |
| 475 | if (player.playerUrl) str += Meta("twitter:player", player.playerUrl); | |
| 476 | if (player.streamUrl) { | |
| 477 | str += Meta("twitter:player:stream", player.streamUrl); | |
| 478 | } | |
| 479 | if (player.width) str += Meta("twitter:player:width", player.width); | |
| 480 | if (player.height) str += Meta("twitter:player:height", player.height); | |
| 481 | } | |
| 482 | } | |
| 483 | if (twitter.card === "app") { | |
| 484 | for (const type of ["iphone", "ipad", "googleplay"]) { | |
| 485 | if (twitter.app.id[type]) { | |
| 486 | str += Meta(`twitter:app:name:${type}`, twitter.app.name); | |
| 487 | str += Meta(`twitter:app:id:${type}`, twitter.app.id[type]); | |
| 488 | } | |
| 489 | if (twitter.app.url?.[type]) { | |
| 490 | str += Meta(`twitter:app:url:${type}`, twitter.app.url[type]); | |
| 491 | } | |
| 492 | } | |
| 493 | } | |
| 494 | } | |
| 495 | ||
| 496 | // <AppLinksMeta /> | |
| 497 | if (meta.appLinks) { | |
| 498 | if (meta.appLinks.ios) { | |
| 499 | for (var item of meta.appLinks.ios) { | |
| 500 | str += ExtendMeta("al:ios", item); | |
| 501 | } | |
| 502 | } | |
| 503 | if (meta.appLinks.iphone) { | |
| 504 | for (var item of meta.appLinks.iphone) { | |
| 505 | str += ExtendMeta("al:iphone", item); | |
| 506 | } | |
| 507 | } | |
| 508 | if (meta.appLinks.ipad) { | |
| 509 | for (var item of meta.appLinks.ipad) { | |
| 510 | str += ExtendMeta("al:ipad", item); | |
| 511 | } | |
| 512 | } | |
| 513 | if (meta.appLinks.android) { | |
| 514 | for (var item2 of meta.appLinks.android) { | |
| 515 | str += ExtendMeta("al:android", item2); | |
| 516 | } | |
| 517 | } | |
| 518 | if (meta.appLinks.windows_phone) { | |
| 519 | for (var item3 of meta.appLinks.windows_phone) { | |
| 520 | str += ExtendMeta("al:windows_phone", item3); | |
| 521 | } | |
| 522 | } | |
| 523 | if (meta.appLinks.windows) { | |
| 524 | for (var item3 of meta.appLinks.windows) { | |
| 525 | str += ExtendMeta("al:windows", item3); | |
| 526 | } | |
| 527 | } | |
| 528 | if (meta.appLinks.windows_universal) { | |
| 529 | for (var item4 of meta.appLinks.windows_universal) { | |
| 530 | str += ExtendMeta("al:windows_universal", item4); | |
| 531 | } | |
| 532 | } | |
| 533 | if (meta.appLinks.web) { | |
| 534 | for (const item of meta.appLinks.web) { | |
| 535 | str += ExtendMeta("al:web", item); | |
| 536 | } | |
| 537 | } | |
| 538 | } | |
| 539 | ||
| 540 | // <IconsMetadata /> | |
| 541 | if (meta.icons) { | |
| 542 | if (meta.icons.shortcut) { | |
| 543 | for (var icon of meta.icons.shortcut) { | |
| 544 | str += IconLink("shortcut icon", icon); | |
| 545 | } | |
| 546 | } | |
| 547 | if (meta.icons.icon) { | |
| 548 | for (var icon of meta.icons.icon) { | |
| 549 | str += IconLink("icon", icon); | |
| 550 | } | |
| 551 | } | |
| 552 | if (meta.icons.apple) { | |
| 553 | for (var icon of meta.icons.apple) { | |
| 554 | str += IconLink("apple-touch-icon", icon); | |
| 555 | } | |
| 556 | } | |
| 557 | if (meta.icons.other) { | |
| 558 | for (var icon of meta.icons.other) { | |
| 559 | str += IconLink(icon.rel ?? "icon", icon); | |
| 560 | } | |
| 561 | } | |
| 562 | } | |
| 563 | ||
| 564 | return str; | |
| 565 | } |
framework/meta/types.ts created+57| ... | ... | @@ -0,0 +1,57 @@ |
| 1 | export type { | |
| 2 | AlternateURLs, | |
| 3 | ResolvedAlternateURLs, | |
| 4 | } from "./nextjs/types/alternative-urls-types"; | |
| 5 | export type { | |
| 6 | AppleImage, | |
| 7 | AppleImageDescriptor, | |
| 8 | AppleWebApp, | |
| 9 | AppLinks, | |
| 10 | AppLinksAndroid, | |
| 11 | AppLinksApple, | |
| 12 | AppLinksWeb, | |
| 13 | AppLinksWindows, | |
| 14 | FormatDetection, | |
| 15 | ItunesApp, | |
| 16 | ResolvedAppleWebApp, | |
| 17 | ResolvedAppLinks, | |
| 18 | Viewport, | |
| 19 | } from "./nextjs/types/extra-types"; | |
| 20 | export type { | |
| 21 | Metadata, | |
| 22 | ResolvedMetadata, | |
| 23 | ResolvingMetadata, | |
| 24 | } from "./nextjs/types/metadata-interface"; | |
| 25 | export type { | |
| 26 | AbsoluteString, | |
| 27 | AbsoluteTemplateString, | |
| 28 | Author, | |
| 29 | ColorSchemeEnum, | |
| 30 | DefaultTemplateString, | |
| 31 | Icon, | |
| 32 | IconDescriptor, | |
| 33 | Icons, | |
| 34 | IconURL, | |
| 35 | ReferrerEnum, | |
| 36 | ResolvedIcons, | |
| 37 | ResolvedRobots, | |
| 38 | ResolvedVerification, | |
| 39 | Robots, | |
| 40 | TemplateString, | |
| 41 | ThemeColorDescriptor, | |
| 42 | Verification, | |
| 43 | } from "./nextjs/types/metadata-types"; | |
| 44 | export type { | |
| 45 | OpenGraph, | |
| 46 | OpenGraphType, | |
| 47 | ResolvedOpenGraph, | |
| 48 | } from "./nextjs/types/opengraph-types"; | |
| 49 | export type { | |
| 50 | FieldResolver, | |
| 51 | FieldResolverWithMetadataBase, | |
| 52 | } from "./nextjs/types/resolvers"; | |
| 53 | export type { | |
| 54 | ResolvedTwitterMetadata, | |
| 55 | Twitter, | |
| 56 | TwitterAppDescriptor, | |
| 57 | } from "./nextjs/types/twitter-types"; |
framework/meta/utils.ts created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | // Extracted from @paperdave/utils/string | |
| 2 | declare var Bun: any; | |
| 3 | ||
| 4 | export const escapeHTML: (string: string) => string = typeof Bun !== "undefined" | |
| 5 | ? Bun.escapeHTML | |
| 6 | : (string: string) => { | |
| 7 | return string | |
| 8 | .replaceAll('"', "&quot;") | |
| 9 | .replaceAll("&", "&amp;") | |
| 10 | .replaceAll("'", "&#x27;") | |
| 11 | .replaceAll("<", "&lt;") | |
| 12 | .replaceAll(">", "&gt;"); | |
| 13 | }; |
framework/mime.ts created+30| ... | ... | @@ -0,0 +1,30 @@ |
| 1 | const db = new Map( | |
| 2 | fs.readFileSync(path.join(import.meta.dirname, "mime.txt"), "utf8") | |
| 3 | .split("\n").filter(Boolean).map((line) => | |
| 4 | line.split(/\s+/) as [string, string] | |
| 5 | ), | |
| 6 | ); | |
| 7 | ||
| 8 | /** | |
| 9 | * Accepts: | |
| 10 | * - Full file path | |
| 11 | * - Extension (with or without dot) | |
| 12 | */ | |
| 13 | export function contentTypeFor(file: string) { | |
| 14 | if (file.includes("/") || file.includes("\\")) { | |
| 15 | // Some file names are special cased. | |
| 16 | switch (path.basename(file)) { | |
| 17 | case "rss.xml": | |
| 18 | return "application/rss+xml"; | |
| 19 | } | |
| 20 | ||
| 21 | file = path.extname(file); | |
| 22 | } | |
| 23 | const dot = file.indexOf("."); | |
| 24 | if (dot === -1) file = "." + file; | |
| 25 | else if (dot > 0) file = file.slice(dot); | |
| 26 | return db.get(file) ?? "application/octet-stream"; | |
| 27 | } | |
| 28 | ||
| 29 | import * as fs from "./fs.ts"; | |
| 30 | import * as path from "node:path"; |
framework/mime.txt created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | .css text/css | |
| 2 | .html text/html; charset=utf8 | |
| 3 | .jpeg image/jpeg | |
| 4 | .jpg image/jpeg | |
| 5 | .js text/javascript | |
| 6 | .json application/json | |
| 7 | .png image/png | |
| 8 | .txt text/plain |
framework/queue.ts created+206| ... | ... | @@ -0,0 +1,206 @@ |
| 1 | import { Progress } from "@paperclover/console/Progress"; | |
| 2 | import { Spinner } from "@paperclover/console/Spinner"; | |
| 3 | import * as path from "node:path"; | |
| 4 | import process from "node:process"; | |
| 5 | ||
| 6 | interface QueueOptions<T, R> { | |
| 7 | name: string; | |
| 8 | fn: (item: T, spin: Spinner) => Promise<R>; | |
| 9 | getItemText?: (item: T) => string; | |
| 10 | maxJobs?: number; | |
| 11 | passive?: boolean; | |
| 12 | } | |
| 13 | ||
| 14 | // Process multiple items in parallel, queue up as many. | |
| 15 | export class Queue<T, R> { | |
| 16 | #name: string; | |
| 17 | #fn: (item: T, spin: Spinner) => Promise<R>; | |
| 18 | #maxJobs: number; | |
| 19 | #getItemText: (item: T) => string; | |
| 20 | #passive: boolean; | |
| 21 | ||
| 22 | #active: Spinner[] = []; | |
| 23 | #queue: Array<[T] | [T, (result: R) => void, (err: unknown) => void]> = []; | |
| 24 | ||
| 25 | #cachedProgress: Progress<{ active: Spinner[] }> | null = null; | |
| 26 | #done: number = 0; | |
| 27 | #total: number = 0; | |
| 28 | #onComplete: (() => void) | null = null; | |
| 29 | #estimate: number | null = null; | |
| 30 | #errors: unknown[] = []; | |
| 31 | ||
| 32 | constructor(options: QueueOptions<T, R>) { | |
| 33 | this.#name = options.name; | |
| 34 | this.#fn = options.fn; | |
| 35 | this.#maxJobs = options.maxJobs ?? 5; | |
| 36 | this.#getItemText = options.getItemText ?? defaultGetItemText; | |
| 37 | this.#passive = options.passive ?? false; | |
| 38 | } | |
| 39 | ||
| 40 | get bar() { | |
| 41 | const cached = this.#cachedProgress; | |
| 42 | if (!cached) { | |
| 43 | const bar = this.#cachedProgress = new Progress({ | |
| 44 | spinner: null, | |
| 45 | text: ({ active }) => { | |
| 46 | const now = performance.now(); | |
| 47 | let text = `[${this.#done}/${this.#total}] ${this.#name}`; | |
| 48 | let n = 0; | |
| 49 | for (const item of active) { | |
| 50 | let itemText = "- " + item.format(now); | |
| 51 | text += `\n` + | |
| 52 | itemText.slice(0, Math.max(0, process.stdout.columns - 1)); | |
| 53 | if (n > 10) { | |
| 54 | text += `\n ... + ${active.length - n} more`; | |
| 55 | break; | |
| 56 | } | |
| 57 | n++; | |
| 58 | } | |
| 59 | return text; | |
| 60 | }, | |
| 61 | props: { | |
| 62 | active: [] as Spinner[], | |
| 63 | }, | |
| 64 | }); | |
| 65 | bar.value = 0; | |
| 66 | return bar; | |
| 67 | } | |
| 68 | return cached; | |
| 69 | } | |
| 70 | ||
| 71 | add(args: T) { | |
| 72 | this.#total += 1; | |
| 73 | this.updateTotal(); | |
| 74 | if (this.#active.length > this.#maxJobs) { | |
| 75 | const { promise, resolve, reject } = Promise.withResolvers<R>(); | |
| 76 | this.#queue.push([args, resolve, reject]); | |
| 77 | return promise; | |
| 78 | } | |
| 79 | return this.#run(args); | |
| 80 | } | |
| 81 | ||
| 82 | addMany(items: T[]) { | |
| 83 | this.#total += items.length; | |
| 84 | this.updateTotal(); | |
| 85 | ||
| 86 | const runNowCount = this.#maxJobs - this.#active.length; | |
| 87 | const runNow = items.slice(0, runNowCount); | |
| 88 | const runLater = items.slice(runNowCount); | |
| 89 | this.#queue.push(...runLater.reverse().map<[T]>((x) => [x])); | |
| 90 | runNow.map((item) => this.#run(item).catch(() => {})); | |
| 91 | } | |
| 92 | ||
| 93 | async #run(args: T): Promise<R> { | |
| 94 | const bar = this.bar; | |
| 95 | const itemText = this.#getItemText(args); | |
| 96 | const spinner = new Spinner(itemText); | |
| 97 | spinner.stop(); | |
| 98 | const active = this.#active; | |
| 99 | try { | |
| 100 | active.unshift(spinner); | |
| 101 | bar.props = { active }; | |
| 102 | const result = await this.#fn(args, spinner); | |
| 103 | this.#done++; | |
| 104 | return result; | |
| 105 | } catch (err) { | |
| 106 | if (err && typeof err === "object") { | |
| 107 | (err as any).job = itemText; | |
| 108 | } | |
| 109 | this.#errors.push(err); | |
| 110 | throw err; | |
| 111 | } finally { | |
| 112 | active.splice(active.indexOf(spinner), 1); | |
| 113 | bar.props = { active }; | |
| 114 | bar.value = this.#done; | |
| 115 | ||
| 116 | // Process next item | |
| 117 | const next = this.#queue.shift(); | |
| 118 | if (next) { | |
| 119 | const args = next[0]; | |
| 120 | this.#run(args) | |
| 121 | .then((result) => next[1]?.(result)) | |
| 122 | .catch((err) => next[2]?.(err)); | |
| 123 | } else if (this.#active.length === 0) { | |
| 124 | if (this.#passive) { | |
| 125 | this.bar.stop(); | |
| 126 | this.#cachedProgress = null; | |
| 127 | } | |
| 128 | this.#onComplete?.(); | |
| 129 | } | |
| 130 | } | |
| 131 | } | |
| 132 | ||
| 133 | updateTotal() { | |
| 134 | const bar = this.bar; | |
| 135 | bar.total = Math.max(this.#total, this.#estimate ?? 0); | |
| 136 | } | |
| 137 | ||
| 138 | set estimate(e: number) { | |
| 139 | this.#estimate = e; | |
| 140 | if (this.#cachedProgress) { | |
| 141 | this.updateTotal(); | |
| 142 | } | |
| 143 | } | |
| 144 | ||
| 145 | async done(o: { method: "success" | "stop" }) { | |
| 146 | if (this.#active.length === 0) { | |
| 147 | this.#end(o); | |
| 148 | return; | |
| 149 | } | |
| 150 | ||
| 151 | const { promise, resolve } = Promise.withResolvers<void>(); | |
| 152 | this.#onComplete = resolve; | |
| 153 | await promise; | |
| 154 | this.#end(o); | |
| 155 | } | |
| 156 | ||
| 157 | #end( | |
| 158 | { method = this.#passive ? "stop" : "success" }: { | |
| 159 | method: "success" | "stop"; | |
| 160 | }, | |
| 161 | ) { | |
| 162 | const bar = this.#cachedProgress; | |
| 163 | if (this.#errors.length > 0) { | |
| 164 | if (bar) bar.stop(); | |
| 165 | throw new AggregateError( | |
| 166 | this.#errors, | |
| 167 | this.#errors.length + " jobs failed in '" + this.#name + "'", | |
| 168 | ); | |
| 169 | } | |
| 170 | ||
| 171 | if (bar) bar[method](); | |
| 172 | } | |
| 173 | } | |
| 174 | ||
| 175 | const cwd = process.cwd(); | |
| 176 | function defaultGetItemText(item: unknown) { | |
| 177 | let itemText = ""; | |
| 178 | if (typeof item === "string") { | |
| 179 | itemText = item; | |
| 180 | } else if (typeof item === "object" && item !== null) { | |
| 181 | const { path, label, id } = item as any; | |
| 182 | itemText = label ?? path ?? id ?? JSON.stringify(item); | |
| 183 | } else { | |
| 184 | itemText = JSON.stringify(item); | |
| 185 | } | |
| 186 | ||
| 187 | if (itemText.startsWith(cwd)) { | |
| 188 | itemText = path.relative(cwd, itemText); | |
| 189 | } | |
| 190 | return itemText; | |
| 191 | } | |
| 192 | ||
| 193 | export class OnceMap<T> { | |
| 194 | private ongoing = new Map<string, Promise<T>>(); | |
| 195 | ||
| 196 | get(key: string, compute: () => Promise<T>) { | |
| 197 | if (this.ongoing.has(key)) { | |
| 198 | return this.ongoing.get(key)!; | |
| 199 | } | |
| 200 | ||
| 201 | const result = compute(); | |
| 202 | this.ongoing.set(key, result); | |
| 203 | result.finally(() => this.ongoing.delete(key)); | |
| 204 | return result; | |
| 205 | } | |
| 206 | } |
framework/sitegen-lib.ts created+49| ... | ... | @@ -0,0 +1,49 @@ |
| 1 | // Import this file with 'import * as sg from "#sitegen";' | |
| 2 | export type ScriptId = string; | |
| 3 | ||
| 4 | export interface SitegenRender { | |
| 5 | scripts: Set<ScriptId>; | |
| 6 | } | |
| 7 | ||
| 8 | export function initRender(): SitegenRender { | |
| 9 | return { | |
| 10 | scripts: new Set(), | |
| 11 | }; | |
| 12 | } | |
| 13 | ||
| 14 | export function getRender() { | |
| 15 | return ssr.getUserData<SitegenRender>("sitegen", () => { | |
| 16 | throw new Error( | |
| 17 | "This function can only be used in a page (static or view)", | |
| 18 | ); | |
| 19 | }); | |
| 20 | } | |
| 21 | ||
| 22 | /** Add a client-side script to the page. */ | |
| 23 | export function addScript(id: ScriptId) { | |
| 24 | const srcFile: string = util.getCallSites() | |
| 25 | .find((site) => !site.scriptName.startsWith(import.meta.dirname))! | |
| 26 | .scriptName; | |
| 27 | const filePath = hot.resolveFrom(srcFile, id); | |
| 28 | if ( | |
| 29 | !filePath.endsWith(".client.ts") && | |
| 30 | !filePath.endsWith(".client.tsx") | |
| 31 | ) { | |
| 32 | throw new Error("addScript must be a .client.ts or .client.tsx"); | |
| 33 | } | |
| 34 | getRender().scripts.add(filePath); | |
| 35 | } | |
| 36 | ||
| 37 | export function Script({ src }: { src: ScriptId }) { | |
| 38 | if (!src) throw new Error("Missing 'src' attribute"); | |
| 39 | addScript(src); | |
| 40 | return null; | |
| 41 | } | |
| 42 | ||
| 43 | export interface Section { | |
| 44 | root: string; | |
| 45 | } | |
| 46 | ||
| 47 | import * as ssr from "./engine/ssr.ts"; | |
| 48 | import * as util from "node:util"; | |
| 49 | import * as hot from "./hot.ts"; |
framework/sitegen.tsx created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | // default | |
| 2 | class Hub { | |
| 3 | getCode() {} | |
| 4 | getScope() {} | |
| 5 | addHelper() { | |
| 6 | throw new Error("Helpers are not supported by the default hub."); | |
| 7 | } | |
| 8 | buildError(node, msg, Error = TypeError) { | |
| 9 | return new Error(msg); | |
| 10 | } | |
| 11 | } |
framework/sqlite.ts created+101| ... | ... | @@ -0,0 +1,101 @@ |
| 1 | // Guard against reloads and bundler duplication. | |
| 2 | // @ts-ignore | |
| 3 | const map = globalThis[Symbol.for("clover.db")] ??= new Map< | |
| 4 | string, | |
| 5 | WrappedDatabase | |
| 6 | >(); | |
| 7 | ||
| 8 | export function getDb(file: string) { | |
| 9 | let db: WrappedDatabase | null = map.get(file); | |
| 10 | if (db) return db; | |
| 11 | const fileWithExt = file.includes(".") ? file : file + ".sqlite"; | |
| 12 | db = new WrappedDatabase( | |
| 13 | new DatabaseSync(path.join(".clover/", fileWithExt)), | |
| 14 | ); | |
| 15 | map.set(file, db); | |
| 16 | return db; | |
| 17 | } | |
| 18 | ||
| 19 | export class WrappedDatabase { | |
| 20 | node: DatabaseSync; | |
| 21 | stmtTableMigrate: WeakRef<StatementSync> | null = null; | |
| 22 | ||
| 23 | constructor(node: DatabaseSync) { | |
| 24 | this.node = node; | |
| 25 | this.node.exec(` | |
| 26 | create table if not exists clover_migrations ( | |
| 27 | key text not null primary key, | |
| 28 | version integer not null | |
| 29 | ); | |
| 30 | `); | |
| 31 | } | |
| 32 | ||
| 33 | // TODO: add migration support | |
| 34 | // the idea is you keep `schema` as the new schema but can add | |
| 35 | // migrations to the mix really easily. | |
| 36 | table(name: string, schema: string) { | |
| 37 | let s = this.stmtTableMigrate?.deref(); | |
| 38 | s ?? (this.stmtTableMigrate = new WeakRef( | |
| 39 | s = this.node.prepare(` | |
| 40 | insert or ignore into clover_migrations | |
| 41 | (key, version) values (?, ?); | |
| 42 | `), | |
| 43 | )); | |
| 44 | const { changes, lastInsertRowid } = s.run(name, 1); | |
| 45 | console.log(changes, lastInsertRowid); | |
| 46 | if (changes === 1) { | |
| 47 | this.node.exec(schema); | |
| 48 | } | |
| 49 | } | |
| 50 | ||
| 51 | prepare<Args extends unknown[] = [], Result = unknown>( | |
| 52 | query: string, | |
| 53 | ): Stmt<Args, Result> { | |
| 54 | return new Stmt(this.node.prepare(query)); | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | export class Stmt<Args extends unknown[] = unknown[], Row = unknown> { | |
| 59 | #node: StatementSync; | |
| 60 | #class: any | null = null; | |
| 61 | constructor(node: StatementSync) { | |
| 62 | this.#node = node; | |
| 63 | } | |
| 64 | ||
| 65 | /** Get one row */ | |
| 66 | get(...args: Args): Row | null { | |
| 67 | const item = this.#node.get(...args as any) as Row; | |
| 68 | if (!item) return null; | |
| 69 | const C = this.#class; | |
| 70 | if (C) Object.setPrototypeOf(item, C.prototype); | |
| 71 | return item; | |
| 72 | } | |
| 73 | getNonNull(...args: Args) { | |
| 74 | const item = this.get(...args); | |
| 75 | if (!item) throw new Error("Query returned no result"); | |
| 76 | return item; | |
| 77 | } | |
| 78 | iter(...args: Args): Iterator<Row> { | |
| 79 | return this.array(...args)[Symbol.iterator](); | |
| 80 | } | |
| 81 | /** Get all rows */ | |
| 82 | array(...args: Args): Row[] { | |
| 83 | const array = this.#node.all(...args as any) as Row[]; | |
| 84 | const C = this.#class; | |
| 85 | if (C) array.forEach((item) => Object.setPrototypeOf(item, C.prototype)); | |
| 86 | return array; | |
| 87 | } | |
| 88 | /** Return the number of changes / row ID */ | |
| 89 | run(...args: Args) { | |
| 90 | return this.#node.run(...args as any); | |
| 91 | } | |
| 92 | ||
| 93 | as<R>(Class: { new (): R }): Stmt<Args, R> { | |
| 94 | this.#class = Class; | |
| 95 | return this as any; | |
| 96 | } | |
| 97 | } | |
| 98 | ||
| 99 | import { DatabaseSync, StatementSync } from "node:sqlite"; | |
| 100 | import * as fs from "./fs.ts"; | |
| 101 | import * as path from "node:path"; |
framework/typecheck.ts createdpackage-lock.json created+81| ... | ... | @@ -0,0 +1,81 @@ |
| 1 | // DEFAULT_OPTIONS | |
| 2 | [object Object] | |
| 3 | ||
| 4 | // strip | |
| 5 | function strip(jsonString, options = DEFAULT_OPTIONS) { | |
| 6 | if (typeof jsonString !== "string") { | |
| 7 | throw new TypeError(`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof jsonString}\``); | |
| 8 | } | |
| 9 | const { trailingCommas = false, whitespace = true } = options; | |
| 10 | const _strip = whitespace ? stripWithWhitespace : stripWithoutWhitespace; | |
| 11 | let isInsideString = false; | |
| 12 | let isInsideComment = false; | |
| 13 | let offset = 0; | |
| 14 | let buffer = ""; | |
| 15 | let result = ""; | |
| 16 | let commaIndex = -1; | |
| 17 | for (let i = 0; i < jsonString.length; i++) { | |
| 18 | const currentCharacter = jsonString[i]; | |
| 19 | const nextCharacter = jsonString[i + 1]; | |
| 20 | if (!currentCharacter) { | |
| 21 | continue; | |
| 22 | } | |
| 23 | if (!isInsideComment && currentCharacter === '"') { | |
| 24 | const escaped = isEscaped(jsonString, i); | |
| 25 | if (!escaped) { | |
| 26 | isInsideString = !isInsideString; | |
| 27 | } | |
| 28 | } | |
| 29 | if (isInsideString) { | |
| 30 | continue; | |
| 31 | } | |
| 32 | if (!isInsideComment && currentCharacter + nextCharacter === "//") { | |
| 33 | buffer += jsonString.slice(offset, i); | |
| 34 | offset = i; | |
| 35 | isInsideComment = singleComment; | |
| 36 | i++; | |
| 37 | } else if (isInsideComment === singleComment && currentCharacter + nextCharacter === "\r\n") { | |
| 38 | i++; | |
| 39 | isInsideComment = false; | |
| 40 | buffer += _strip(jsonString, offset, i); | |
| 41 | offset = i; | |
| 42 | continue; | |
| 43 | } else if (isInsideComment === singleComment && currentCharacter === "\n") { | |
| 44 | isInsideComment = false; | |
| 45 | buffer += _strip(jsonString, offset, i); | |
| 46 | offset = i; | |
| 47 | } else if (!isInsideComment && currentCharacter + nextCharacter === "/*") { | |
| 48 | buffer += jsonString.slice(offset, i); | |
| 49 | offset = i; | |
| 50 | isInsideComment = multiComment; | |
| 51 | i++; | |
| 52 | continue; | |
| 53 | } else if (isInsideComment === multiComment && currentCharacter + nextCharacter === "*/") { | |
| 54 | i++; | |
| 55 | isInsideComment = false; | |
| 56 | buffer += _strip(jsonString, offset, i + 1); | |
| 57 | offset = i + 1; | |
| 58 | continue; | |
| 59 | } else if (trailingCommas && !isInsideComment) { | |
| 60 | if (commaIndex !== -1) { | |
| 61 | if (currentCharacter === "}" || currentCharacter === "]") { | |
| 62 | buffer += jsonString.slice(offset, i); | |
| 63 | result += _strip(buffer, 0, 1) + buffer.slice(1); | |
| 64 | buffer = ""; | |
| 65 | offset = i; | |
| 66 | commaIndex = -1; | |
| 67 | } else if (currentCharacter !== " " && currentCharacter !== "	" && currentCharacter !== "\r" && currentCharacter !== "\n") { | |
| 68 | buffer += jsonString.slice(offset, i); | |
| 69 | offset = i; | |
| 70 | commaIndex = -1; | |
| 71 | } | |
| 72 | } else if (currentCharacter === ",") { | |
| 73 | result += buffer + jsonString.slice(offset, i); | |
| 74 | buffer = ""; | |
| 75 | offset = i; | |
| 76 | commaIndex = i; | |
| 77 | } | |
| 78 | } | |
| 79 | } | |
| 80 | return result + buffer + (isInsideComment ? _strip(jsonString.slice(offset)) : jsonString.slice(offset)); | |
| 81 | } |
package.json created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | { | |
| 2 | "private": true, | |
| 3 | "type": "module", | |
| 4 | "dependencies": { | |
| 5 | "@hono/node-server": "^1.14.3", | |
| 6 | "@mdx-js/mdx": "^3.1.0", | |
| 7 | "@paperclover/console": "git+https://git.paperclover.net/clo/console.git", | |
| 8 | "esbuild": "^0.25.5", | |
| 9 | "hls.js": "^1.6.5", | |
| 10 | "hono": "^4.7.11", | |
| 11 | "marko": "^6.0.20" | |
| 12 | }, | |
| 13 | "devDependencies": { | |
| 14 | "@types/node": "^22.15.29", | |
| 15 | "typescript": "^5.8.3" | |
| 16 | }, | |
| 17 | "imports": { | |
| 18 | "#sitegen": "./framework/sitegen-lib.ts", | |
| 19 | "#sqlite": "./framework/sqlite.ts", | |
| 20 | "#ssr": "./framework/engine/ssr.ts", | |
| 21 | "#ssr/jsx-dev-runtime": "./framework/engine/jsx-runtime.ts", | |
| 22 | "#ssr/jsx-runtime": "./framework/engine/jsx-runtime.ts", | |
| 23 | "#ssr/marko": "./framework/engine/marko-runtime.ts" | |
| 24 | } | |
| 25 | } |
repl.js created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | // default | |
| 2 | (node, parent, grandparent) => { | |
| 3 | switch (parent.type) { | |
| 4 | case "MarkoTag": | |
| 5 | return parent.var !== node; | |
| 6 | case "MarkoTagBody": | |
| 7 | return false; | |
| 8 | default: | |
| 9 | return originalIsReferenced(node, parent, grandparent); | |
| 10 | } | |
| 11 | } |
run.js created+38| ... | ... | @@ -0,0 +1,38 @@ |
| 1 | // This file allows using Node.js in combination with | |
| 2 | // available plugins. Usage: "node run <script>" | |
| 3 | import * as path from "node:path"; | |
| 4 | import process from "node:process"; | |
| 5 | ||
| 6 | // Disable experimental warnings (Type Stripping, etc) | |
| 7 | { | |
| 8 | const { emit: originalEmit } = process; | |
| 9 | const warnings = ["ExperimentalWarning"]; | |
| 10 | process.emit = function (event, error) { | |
| 11 | return event === "warning" && warnings.includes(error.name) | |
| 12 | ? false | |
| 13 | : originalEmit.apply(process, arguments); | |
| 14 | }; | |
| 15 | } | |
| 16 | ||
| 17 | // Init hooks | |
| 18 | const hot = await import("./framework/hot.ts"); | |
| 19 | ||
| 20 | const console = hot.load("@paperclover/console"); | |
| 21 | globalThis.console.log = console.info; | |
| 22 | globalThis.console.info = console.info; | |
| 23 | globalThis.console.warn = console.warn; | |
| 24 | globalThis.console.error = console.error; | |
| 25 | globalThis.console.debug = console.scoped("dbg"); | |
| 26 | ||
| 27 | // Load with hooks | |
| 28 | if (process.argv[1].startsWith(import.meta.filename.slice(0, -".js".length))) { | |
| 29 | if (process.argv.length == 2) { | |
| 30 | console.error("usage: node run <script> [...args]"); | |
| 31 | process.exit(1); | |
| 32 | } | |
| 33 | const file = path.resolve(process.argv[2]); | |
| 34 | process.argv = [process.argv[0], ...process.argv.slice(2)]; | |
| 35 | hot.load(file).main?.(); | |
| 36 | } | |
| 37 | ||
| 38 | export { hot }; |
src/components/Video.tsx createdsrc/components/video.client.ts created+29| ... | ... | @@ -0,0 +1,29 @@ |
| 1 | import type Hls from "hls.js"; | |
| 2 | ||
| 3 | let cachedHls: null | Promise<typeof Hls | false> | typeof Hls | false = null; | |
| 4 | ||
| 5 | const loadHls = () => | |
| 6 | cachedHls ?? (cachedHls = ( | |
| 7 | // @ts-ignore | |
| 8 | +"HLS POLYFILL", import("hls.js") | |
| 9 | ).then((m, y = m.default) => cachedHls = y.isSupported() && y)); | |
| 10 | ||
| 11 | const createVideoPlayer = async (videoElement: HTMLVideoElement) => { | |
| 12 | const sources = videoElement.querySelectorAll("source"); | |
| 13 | for (let i = 0; i < sources.length; i++) { | |
| 14 | const { src, type } = sources[i]; | |
| 15 | if (videoElement.canPlayType(type)) { | |
| 16 | videoElement.src = src; | |
| 17 | return; | |
| 18 | } | |
| 19 | ||
| 20 | if (type === "application/x-mpegURL" && await loadHls()) { | |
| 21 | const hls = new (cachedHls as typeof Hls)(); | |
| 22 | hls.loadSource(src); | |
| 23 | hls.attachMedia(videoElement); | |
| 24 | return; | |
| 25 | } | |
| 26 | } | |
| 27 | }; | |
| 28 | ||
| 29 | document.querySelectorAll("video").forEach(createVideoPlayer); |
src/file-viewer/backend.tsx created+29| ... | ... | @@ -0,0 +1,29 @@ |
| 1 | // default | |
| 2 | function transpileNamespace(path, allowNamespaces) { | |
| 3 | if (path.node.declare || path.node.id.type === "StringLiteral") { | |
| 4 | path.remove(); | |
| 5 | return; | |
| 6 | } | |
| 7 | if (!allowNamespaces) { | |
| 8 | throw path.get("id").buildCodeFrameError("Namespace not marked type-only declare." + " Non-declarative namespaces are only supported experimentally in Babel." + " To enable and review caveats see:" + " https://babeljs.io/docs/en/babel-plugin-transform-typescript"); | |
| 9 | } | |
| 10 | const name = getFirstIdentifier(path.node.id).name; | |
| 11 | const value = handleNested(path, path.node); | |
| 12 | if (value === null) { | |
| 13 | const program = path.findParent(p => p.isProgram()); | |
| 14 | (0, _globalTypes.registerGlobalType)(program.scope, name); | |
| 15 | path.remove(); | |
| 16 | } else if (path.scope.hasOwnBinding(name)) { | |
| 17 | path.replaceWith(value); | |
| 18 | } else { | |
| 19 | path.scope.registerDeclaration(path.replaceWithMultiple([getDeclaration(name), value])[0]); | |
| 20 | } | |
| 21 | } | |
| 22 | ||
| 23 | // getFirstIdentifier | |
| 24 | function getFirstIdentifier(node) { | |
| 25 | if (_core.types.isIdentifier(node)) { | |
| 26 | return node; | |
| 27 | } | |
| 28 | return getFirstIdentifier(node.left); | |
| 29 | } |
src/file-viewer/cache.ts created+36| ... | ... | @@ -0,0 +1,36 @@ |
| 1 | // beginHiddenCallStack | |
| 2 | function beginHiddenCallStack(fn) { | |
| 3 | if (!SUPPORTED) return fn; | |
| 4 | return Object.defineProperty(function (...args) { | |
| 5 | setupPrepareStackTrace(); | |
| 6 | return fn(...args); | |
| 7 | }, "name", { | |
| 8 | value: STOP_HIDING | |
| 9 | }); | |
| 10 | } | |
| 11 | ||
| 12 | // endHiddenCallStack | |
| 13 | function endHiddenCallStack(fn) { | |
| 14 | if (!SUPPORTED) return fn; | |
| 15 | return Object.defineProperty(function (...args) { | |
| 16 | return fn(...args); | |
| 17 | }, "name", { | |
| 18 | value: START_HIDING | |
| 19 | }); | |
| 20 | } | |
| 21 | ||
| 22 | // expectedError | |
| 23 | function expectedError(error) { | |
| 24 | if (!SUPPORTED) return; | |
| 25 | expectedErrors.add(error); | |
| 26 | return error; | |
| 27 | } | |
| 28 | ||
| 29 | // injectVirtualStackFrame | |
| 30 | function injectVirtualStackFrame(error, filename) { | |
| 31 | if (!SUPPORTED) return; | |
| 32 | let frames = virtualFrames.get(error); | |
| 33 | if (!frames) virtualFrames.set(error, frames = []); | |
| 34 | frames.push(CallSite(filename)); | |
| 35 | return error; | |
| 36 | } |
src/file-viewer/cert.pem created+91| ... | ... | @@ -0,0 +1,91 @@ |
| 1 | // default | |
| 2 | function* loadPrivatePartialConfig(inputOpts) { | |
| 3 | if (inputOpts != null && (typeof inputOpts !== "object" || Array.isArray(inputOpts))) { | |
| 4 | throw new Error("Babel options must be an object, null, or undefined"); | |
| 5 | } | |
| 6 | const args = inputOpts ? (0, _options.validate)("arguments", inputOpts) : {}; | |
| 7 | const { | |
| 8 | envName = (0, _environment.getEnv)(), | |
| 9 | cwd = ".", | |
| 10 | root: rootDir = ".", | |
| 11 | rootMode = "root", | |
| 12 | caller, | |
| 13 | cloneInputAst = true | |
| 14 | } = args; | |
| 15 | const absoluteCwd = _path().resolve(cwd); | |
| 16 | const absoluteRootDir = resolveRootMode(_path().resolve(absoluteCwd, rootDir), rootMode); | |
| 17 | const filename = typeof args.filename === "string" ? _path().resolve(cwd, args.filename) : undefined; | |
| 18 | const showConfigPath = yield* (0, _index.resolveShowConfigPath)(absoluteCwd); | |
| 19 | const context = { | |
| 20 | filename, | |
| 21 | cwd: absoluteCwd, | |
| 22 | root: absoluteRootDir, | |
| 23 | envName, | |
| 24 | caller, | |
| 25 | showConfig: showConfigPath === filename | |
| 26 | }; | |
| 27 | const configChain = yield* (0, _configChain.buildRootChain)(args, context); | |
| 28 | if (!configChain) return null; | |
| 29 | const merged = { | |
| 30 | assumptions: {} | |
| 31 | }; | |
| 32 | configChain.options.forEach(opts => { | |
| 33 | (0, _util.mergeOptions)(merged, opts); | |
| 34 | }); | |
| 35 | const options = Object.assign({}, merged, { | |
| 36 | targets: (0, _resolveTargets.resolveTargets)(merged, absoluteRootDir), | |
| 37 | cloneInputAst, | |
| 38 | babelrc: false, | |
| 39 | configFile: false, | |
| 40 | browserslistConfigFile: false, | |
| 41 | passPerPreset: false, | |
| 42 | envName: context.envName, | |
| 43 | cwd: context.cwd, | |
| 44 | root: context.root, | |
| 45 | rootMode: "root", | |
| 46 | filename: typeof context.filename === "string" ? context.filename : undefined, | |
| 47 | plugins: configChain.plugins.map(descriptor => (0, _item.createItemFromDescriptor)(descriptor)), | |
| 48 | presets: configChain.presets.map(descriptor => (0, _item.createItemFromDescriptor)(descriptor)) | |
| 49 | }); | |
| 50 | return { | |
| 51 | options, | |
| 52 | context, | |
| 53 | fileHandling: configChain.fileHandling, | |
| 54 | ignore: configChain.ignore, | |
| 55 | babelrc: configChain.babelrc, | |
| 56 | config: configChain.config, | |
| 57 | files: configChain.files | |
| 58 | }; | |
| 59 | } | |
| 60 | ||
| 61 | // loadPartialConfig | |
| 62 | function* loadPartialConfig(opts) { | |
| 63 | let showIgnoredFiles = false; | |
| 64 | if (typeof opts === "object" && opts !== null && !Array.isArray(opts)) { | |
| 65 | var _opts = opts; | |
| 66 | ({ | |
| 67 | showIgnoredFiles | |
| 68 | } = _opts); | |
| 69 | opts = _objectWithoutPropertiesLoose(_opts, _excluded); | |
| 70 | _opts; | |
| 71 | } | |
| 72 | const result = yield* loadPrivatePartialConfig(opts); | |
| 73 | if (!result) return null; | |
| 74 | const { | |
| 75 | options, | |
| 76 | babelrc, | |
| 77 | ignore, | |
| 78 | config, | |
| 79 | fileHandling, | |
| 80 | files | |
| 81 | } = result; | |
| 82 | if (fileHandling === "ignored" && !showIgnoredFiles) { | |
| 83 | return null; | |
| 84 | } | |
| 85 | (options.plugins || []).forEach(item => { | |
| 86 | if (item.value instanceof _plugin.default) { | |
| 87 | throw new Error("Passing cached plugin instances is not supported in " + "babel.loadPartialConfig()"); | |
| 88 | } | |
| 89 | }); | |
| 90 | return new PartialConfig(options, babelrc ? babelrc.filepath : undefined, ignore ? ignore.filepath : undefined, config ? config.filepath : undefined, fileHandling, files); | |
| 91 | } |
src/file-viewer/cotyledon.tsx created+291| ... | ... | @@ -0,0 +1,291 @@ |
| 1 | export function Speedbump() { | |
| 2 | return ( | |
| 3 | <div class="panel last"> | |
| 4 | <div className="header"> | |
| 5 | an interlude | |
| 6 | </div> | |
| 7 | <div className="content file-view file-view-text speedbump"> | |
| 8 | <canvas | |
| 9 | style="linear-gradient(45deg, #111318, #181f20)" | |
| 10 | data-canvas="cotyledon" | |
| 11 | > | |
| 12 | </canvas> | |
| 13 | <header> | |
| 14 | <h1>cotyledon</h1> | |
| 15 | </header> | |
| 16 | <div id="captcha" style="display: none;"> | |
| 17 | <p style="max-width:480px"> | |
| 18 | please prove you're not a robot by selecting all of the images with | |
| 19 | four-leaf clovers, until there are only regular clovers. | |
| 20 | <noscript> | |
| 21 | this will require javascript enabled on your computer to verify | |
| 22 | the mouse clicks. | |
| 23 | </noscript> | |
| 24 | </p> | |
| 25 | <div className="enter-container"> | |
| 26 | <div className="image-grid"> | |
| 27 | <button> | |
| 28 | <img src="/captcha/image/1.jpeg" alt="a four-leaf clover" /> | |
| 29 | </button> | |
| 30 | <button> | |
| 31 | <img src="/captcha/image/2.jpeg" alt="a four-leaf clover" /> | |
| 32 | </button> | |
| 33 | <button> | |
| 34 | <img src="/captcha/image/3.jpeg" alt="a four-leaf clover" /> | |
| 35 | </button> | |
| 36 | <button> | |
| 37 | <img src="/captcha/image/4.jpeg" alt="a four-leaf clover" /> | |
| 38 | </button> | |
| 39 | <button> | |
| 40 | <img src="/captcha/image/5.jpeg" alt="a four-leaf clover" /> | |
| 41 | </button> | |
| 42 | <button> | |
| 43 | <img src="/captcha/image/6.jpeg" alt="a four-leaf clover" /> | |
| 44 | </button> | |
| 45 | <button> | |
| 46 | <img src="/captcha/image/7.jpeg" alt="a four-leaf clover" /> | |
| 47 | </button> | |
| 48 | <button> | |
| 49 | <img src="/captcha/image/8.jpeg" alt="a four-leaf clover" /> | |
| 50 | </button> | |
| 51 | <button> | |
| 52 | <img src="/captcha/image/9.jpeg" alt="a four-leaf clover" /> | |
| 53 | </button> | |
| 54 | </div> | |
| 55 | </div> | |
| 56 | <div class="enter-container"> | |
| 57 | <button id="enter2">all done</button> | |
| 58 | </div> | |
| 59 | </div> | |
| 60 | <div id="first"> | |
| 61 | <p> | |
| 62 | this place is sacred, but dangerous. i have to keep visitors to an | |
| 63 | absolute minimum; you'll get dust on all the artifacts. | |
| 64 | </p> | |
| 65 | <p> | |
| 66 | by entering our museum, you agree not to use your camera. flash off | |
| 67 | isn't enough; the bits and bytes are alergic even to a camera's | |
| 68 | sensor | |
| 69 | </p> | |
| 70 | <p style="font-size:0.9rem;"> | |
| 71 | (in english: please do not store downloads after you're done viewing | |
| 72 | them) | |
| 73 | </p> | |
| 74 | <div class="enter-container"> | |
| 75 | <button id="enter">break my boundaries</button> | |
| 76 | </div> | |
| 77 | </div> | |
| 78 | </div> | |
| 79 | </div> | |
| 80 | ); | |
| 81 | } | |
| 82 | ||
| 83 | export function Readme() { | |
| 84 | return ( | |
| 85 | <div class="panel last"> | |
| 86 | <div className="header"> | |
| 87 | cotyledon | |
| 88 | </div> | |
| 89 | <div className="content file-view file-view-text"> | |
| 90 | <div style="max-width: 71ch;padding:3rem;font-family:rmo,monospace"> | |
| 91 | <p style="margin-top:0"> | |
| 92 | welcome to the archive. if this is your first time here, i recommend | |
| 93 | starting in '<a href="/file/2017">2017</a>' and going | |
| 94 | chronologically from there. however, there is truly no wrong way to | |
| 95 | explore. | |
| 96 | </p> | |
| 97 | <p> | |
| 98 | note that there is a blanket trigger warning for everything in this | |
| 99 | archive: while there is nothing visually offensive, some portions of | |
| 100 | the text and emotions conveyed through this may hit extremely hard. | |
| 101 | you are warned. | |
| 102 | </p> | |
| 103 | <p> | |
| 104 | all file dates are real. at least as real as i could figure out. | |
| 105 | when i moved data across drives over my years, i accidentally had a | |
| 106 | few points where i stamped over all the dates with the day that | |
| 107 | moved the files. even fucked it up a final time in february 2025, | |
| 108 | while in the process of unfucking things. | |
| 109 | </p> | |
| 110 | <p> | |
| 111 | thankfully, my past self knew i'd want to assemble this kind of | |
| 112 | site, and because of that they were crazy about storing the dates of | |
| 113 | things inside of html, json/yaml files, and even in fucking | |
| 114 | databases. i'm glad it was all stored though, but jeez what a nerd. | |
| 115 | </p> | |
| 116 | <p> | |
| 117 | a few files were touched up for privacy, or otherwise re-encoded. | |
| 118 | some of them i added extra metadata. | |
| 119 | </p> | |
| 120 | <p> | |
| 121 | from the bottom of my heart: i hope you enjoy. it has been a | |
| 122 | nightmare putting this all together. technically and emotionally | |
| 123 | speaking. i'm glad we can put this all behind us, mark it as | |
| 124 | completed, and get started with the good shit. | |
| 125 | </p> | |
| 126 | <p> | |
| 127 | love,<br />clo | |
| 128 | </p> | |
| 129 | <br /> | |
| 130 | <p> | |
| 131 | start here -&gt; <a href="/file/2017">2017</a> | |
| 132 | </p> | |
| 133 | </div> | |
| 134 | </div> | |
| 135 | </div> | |
| 136 | ); | |
| 137 | } | |
| 138 | ||
| 139 | export function ForEveryone() { | |
| 140 | // deno-fmt-ignore | |
| 141 | return <><div class="for_everyone"> | |
| 142 | <p>today is my 21st birthday. april 30th, 2025.</p> | |
| 143 | <p>it's been nearly six months starting hormones.</p> | |
| 144 | <p>sometimes i feel great,</p> | |
| 145 | <p>sometimes i get dysphoria.</p> | |
| 146 | <p>with the walls around me gone</p> | |
| 147 | <p>that shit hits way harder than it did before.</p> | |
| 148 | <p>ugh..</p> | |
| 149 | <p>i'm glad the pain i felt is now explained,</p> | |
| 150 | <p>but now rendered in high definition.</p> | |
| 151 | <p>the smallest strands of hair on my face and belly act</p> | |
| 152 | <p>as sharpened nails to pierce my soul.</p> | |
| 153 | <p></p> | |
| 154 | <p>it's all a pathway to better days; the sun had risen.</p> | |
| 155 | <p>one little step at a time for both of us.</p> | |
| 156 | <p>today i quit my job. free falling, it feels so weird.</p> | |
| 157 | <p>like sky diving.</p> | |
| 158 | <p>the only thing i feel is cold wind.</p> | |
| 159 | <p>the only thing i see is everything,</p> | |
| 160 | <p>and it's beautiful.</p> | |
| 161 | <p>i have a month of falling before the parachute activates,</p> | |
| 162 | <p>gonna spend as much time of it on art as i can.</p> | |
| 163 | <p>that was, after all, my life plan:</p> | |
| 164 | <p>i wanted to make art, all the time,</p> | |
| 165 | <p>for everyone.</p> | |
| 166 | <p></p> | |
| 167 | <p>then you see what happened</p> | |
| 168 | <p>to the world and the internet.</p> | |
| 169 | <p>i never really got to live through that golden age,</p> | |
| 170 | <p>it probably sucked back then too.</p> | |
| 171 | <p>but now the big sites definitely stopped being fun.</p> | |
| 172 | <p>they slide their cold hands up my body</p> | |
| 173 | <p>and feel me around. it's unwelcoming, and</p> | |
| 174 | <p>inconsiderate to how sensitive my skin is.</p> | |
| 175 | <p>i'm so fucking glad i broke up with YouTube</p> | |
| 176 | <p>and their devilish friends.</p> | |
| 177 | <p>my NAS is at 5 / 24 TB</p> | |
| 178 | <p>and probably wont fill for the next decade.</p> | |
| 179 | <p></p> | |
| 180 | <p>it took 2 months for me to notice my body changed.</p> | |
| 181 | <p>that day was really nice, but it hurt a lot.</p> | |
| 182 | <p>a sharp, satisfying pain in my chest gave me life.</p> | |
| 183 | <p>learned new instincts for my arms</p> | |
| 184 | <p>so they'd stop poking my new shape.</p> | |
| 185 | <p>when i look at my face</p> | |
| 186 | <p>it's like a different person.</p> | |
| 187 | <p>she was the same as before, but completely new.</p> | |
| 188 | <p>something changed</p> | |
| 189 | <p>or i'm now used to seeing what makes me smile.</p> | |
| 190 | <p>regardless, whatever i see in the mirror, i smile.</p> | |
| 191 | <p>and, i don't hear that old name much anymore</p> | |
| 192 | <p>aside from nightmares. and you'll never repeat it, ok?</p> | |
| 193 | <p>okay.</p> | |
| 194 | <p></p> | |
| 195 | <p>been playing 'new canaan' by 'bill wurtz' on loop</p> | |
| 196 | <p>in the background.</p> | |
| 197 | <p>it kinda just feels right.</p> | |
| 198 | <p>especially when that verse near the end comes on.</p> | |
| 199 | <p></p> | |
| 200 | <p>more people have been allowed to visit me.</p> | |
| 201 | <p>my apartment used to be just for me,</p> | |
| 202 | <p>but the more i felt like a person</p> | |
| 203 | <p>the more i felt like having others over.</p> | |
| 204 | <p>still have to decorate and clean it a little,</p> | |
| 205 | <p>but it isn't a job to do alone.</p> | |
| 206 | <p>we dragged a giant a rug across the city one day,</p> | |
| 207 | <p>and it felt was like anything was possible.</p> | |
| 208 | <p>sometimes i have ten people visit in a day,</p> | |
| 209 | <p>or sometimes i focus my little eyes on just one.</p> | |
| 210 | <p>i never really know what i want to do</p> | |
| 211 | <p>until the time actually comes.</p> | |
| 212 | <p></p> | |
| 213 | {/* FILIP */} | |
| 214 | <p>i think about the times i was by the water with you.</p> | |
| 215 | <p>the sun setting warmly, icy air fell on our shoulders.</p> | |
| 216 | {/* NATALIE */} | |
| 217 | <p>and how we walked up to the top of that hill,</p> | |
| 218 | <p>you picked up and disposed a nail on the ground,</p> | |
| 219 | <p>walking the city thru places i've never been.</p> | |
| 220 | {/* BEN */} | |
| 221 | <p>or hiking through the park talking about compilers,</p> | |
| 222 | <p>tiring me out until i'd fall asleep in your arms.</p> | |
| 223 | {/* ELENA */} | |
| 224 | <p>and the way you held on to my hand as i woke up,</p> | |
| 225 | <p>noticing how i was trying to hide nightmare's tears.</p> | |
| 226 | <p></p> | |
| 227 | {/* HIGH SCHOOL */} | |
| 228 | <p>i remember we were yelling lyrics loudly,</p> | |
| 229 | <p>out of key yet cheered on because it was fun.</p> | |
| 230 | {/* ADVAITH/NATALIE */} | |
| 231 | <p>and when we all toured the big corporate office,</p> | |
| 232 | {/* AYU/HARRIS */} | |
| 233 | <p>then snuck in to some startup's office after hours;</p> | |
| 234 | <p>i don't remember what movie we watched.</p> | |
| 235 | {/* COLLEGE, DAY 1 IN EV's ROOM */} | |
| 236 | <p>i remember laying on the bunk bed,</p> | |
| 237 | <p>while the rest played a card game.</p> | |
| 238 | {/* MEGHAN/MORE */} | |
| 239 | <p>with us all laying on the rug, staring at the TV</p> | |
| 240 | <p>as the ending twist to that show was revealed.</p> | |
| 241 | <p></p> | |
| 242 | <p>all the moments i cherish,</p> | |
| 243 | <p>i love because it was always me.</p> | |
| 244 | <p>i didn't have to pretend,</p> | |
| 245 | <p>even if i didn't know who i was at the time.</p> | |
| 246 | <p>you all were there. for me.</p> | |
| 247 | <p></p> | |
| 248 | <p>i don't want to pretend any more</p> | |
| 249 | <p>i want to be myself. for everyone.</p> | |
| 250 | <p></p> | |
| 251 | <p>oh, the song ended. i thought it was on loop?</p> | |
| 252 | <p>it's late... can hear the crickets...</p> | |
| 253 | <p>and i can almost see the moon... mmmm...</p> | |
| 254 | <p>...nah, too much light pollution.</p> | |
| 255 | <p></p> | |
| 256 | <p>one day. one day.</p> | |
| 257 | <p></p> | |
| 258 | <p class="normal">before i go, i want to show the uncensored version of "journal about a girl", because i can trust you at least. keep in mind, i think you're one of the first people to ever see this.</p> | |
| 259 | </div> | |
| 260 | <div class="for_everyone" style="max-width:80ch;"> | |
| 261 | <blockquote> | |
| 262 | <p>journal - 2024-09-14</p> | |
| 263 | <p>been at HackMIT today on behalf of the company. it's fun. me and zack were running around looking for people that might be good hires. he had this magic arbitrary criteria to tell "oh this person is probably cracked let's talk to them" and we go to the first one. they were a nerd, perfect. they seemed to be extremely talented with some extreme software projects.<br/> | |
| 264 | okay.. oof... its still clouding my mind<br/> | |
| 265 | i cant shake that feeling away</p> | |
| 266 | <p>hold on...</p> | |
| 267 | <p>at some point they open one of their profiles to navigate to some code, and it displays for a couple of seconds: "pronouns: she/they". i don't actually know anything about this person, but it was my perception that she is trans. their appearance, physique, and age felt similar to me, which tends makes people think you are male.</p> | |
| 268 | <p>but... she was having fun being herself. being a legend of identity and of her skill in computer science. winning the physics major. making cool shit at the hackathon, and probably in life. my perception of her was the exact essence of who i myself wanted to be. i was jealous of her life.</p> | |
| 269 | <p>i tried hard to avoid a breakdown. success. but i was feeling distant. the next hour or so was disorienting, trying not to think about it too hard. i think there was one possibly interesting person we talked to. i don't remember any of the other conversations. they were not important. but i couldn't think through them regardless.</p> | |
| 270 | <p>later, i decided to read some of her code. i either have a huge dislike towards the Rust programming language and/or it was not high quality code. welp, so just is a person studying. my perception was just a perception, inaccurate but impacting. i know i need to become myself, whoever that is. otherwise, i'm just going to feel this shit at higher doses. i think about this every day, and the amount of time i feel being consumed by these problems only grows.</p> | |
| 271 | <p>getting through it all is a lonely feeling. not because no one is around, but because i am isolated emotionally. i know other people hit these feelings, but we all are too afraid to speak up, and it's all lonely.</p> | |
| 272 | <p>waiting on a reply from someone from healthcare. it'll be slow, but it will be okay.</p> | |
| 273 | </blockquote> | |
| 274 | </div> | |
| 275 | <div class="for_everyone"> | |
| 276 | <p class="normal"> | |
| 277 | i've learned that even when i feel alone, it doesn't have to feel lonely. i know it's hard, dear. i know it's scary. but i promise it's possible. we're all in this together. struggling together. sacrificing together. we dedicate our lives to each you, and our art for everyone. | |
| 278 | </p> | |
| 279 | ||
| 280 | <p class="normal" style="font-size:2rem;color:#9C91FF;font-family:times,serif;font-style:italic"> | |
| 281 | and then we knew,<br/> | |
| 282 | just like paper airplanes: that we could fly... | |
| 283 | </p> | |
| 284 | <br /> | |
| 285 | <p class="normal"> | |
| 286 | <a href="/" style='text-decoration:underline;text-underline-offset:0.2em;'>fin.</a> | |
| 287 | </p> | |
| 288 | </div> | |
| 289 | </> | |
| 290 | } | |
| 291 | ForEveryone.class = "text"; |
src/file-viewer/extension-stats.ts created+40| ... | ... | @@ -0,0 +1,40 @@ |
| 1 | // run | |
| 2 | function* run(config, code, ast) { | |
| 3 | const file = yield* (0, _normalizeFile.default)(config.passes, (0, _normalizeOpts.default)(config), code, ast); | |
| 4 | const opts = file.opts; | |
| 5 | try { | |
| 6 | yield* transformFile(file, config.passes); | |
| 7 | } catch (e) { | |
| 8 | var _opts$filename; | |
| 9 | e.message = `${(_opts$filename = opts.filename) != null ? _opts$filename : "unknown file"}: ${e.message}`; | |
| 10 | if (!e.code) { | |
| 11 | e.code = "BABEL_TRANSFORM_ERROR"; | |
| 12 | } | |
| 13 | throw e; | |
| 14 | } | |
| 15 | let outputCode, outputMap; | |
| 16 | try { | |
| 17 | if (opts.code !== false) { | |
| 18 | ({ | |
| 19 | outputCode, | |
| 20 | outputMap | |
| 21 | } = (0, _generate.default)(config.passes, file)); | |
| 22 | } | |
| 23 | } catch (e) { | |
| 24 | var _opts$filename2; | |
| 25 | e.message = `${(_opts$filename2 = opts.filename) != null ? _opts$filename2 : "unknown file"}: ${e.message}`; | |
| 26 | if (!e.code) { | |
| 27 | e.code = "BABEL_GENERATE_ERROR"; | |
| 28 | } | |
| 29 | throw e; | |
| 30 | } | |
| 31 | return { | |
| 32 | metadata: file.metadata, | |
| 33 | options: opts, | |
| 34 | ast: opts.ast === true ? file.ast : null, | |
| 35 | code: outputCode === undefined ? null : outputCode, | |
| 36 | map: outputMap === undefined ? null : outputMap, | |
| 37 | sourceType: file.ast.program.sourceType, | |
| 38 | externalDependencies: (0, _deepArray.flattenToSet)(config.externalDependencies) | |
| 39 | }; | |
| 40 | } |
src/file-viewer/highlight-grammar/astro.plist created+44| ... | ... | @@ -0,0 +1,44 @@ |
| 1 | // default | |
| 2 | function normalizeOptions(config) { | |
| 3 | const { | |
| 4 | filename, | |
| 5 | cwd, | |
| 6 | filenameRelative = typeof filename === "string" ? _path().relative(cwd, filename) : "unknown", | |
| 7 | sourceType = "module", | |
| 8 | inputSourceMap, | |
| 9 | sourceMaps = !!inputSourceMap, | |
| 10 | sourceRoot = config.options.moduleRoot, | |
| 11 | sourceFileName = _path().basename(filenameRelative), | |
| 12 | comments = true, | |
| 13 | compact = "auto" | |
| 14 | } = config.options; | |
| 15 | const opts = config.options; | |
| 16 | const options = Object.assign({}, opts, { | |
| 17 | parserOpts: Object.assign({ | |
| 18 | sourceType: _path().extname(filenameRelative) === ".mjs" ? "module" : sourceType, | |
| 19 | sourceFileName: filename, | |
| 20 | plugins: [] | |
| 21 | }, opts.parserOpts), | |
| 22 | generatorOpts: Object.assign({ | |
| 23 | filename, | |
| 24 | auxiliaryCommentBefore: opts.auxiliaryCommentBefore, | |
| 25 | auxiliaryCommentAfter: opts.auxiliaryCommentAfter, | |
| 26 | retainLines: opts.retainLines, | |
| 27 | comments, | |
| 28 | shouldPrintComment: opts.shouldPrintComment, | |
| 29 | compact, | |
| 30 | minified: opts.minified, | |
| 31 | sourceMaps, | |
| 32 | sourceRoot, | |
| 33 | sourceFileName | |
| 34 | }, opts.generatorOpts) | |
| 35 | }); | |
| 36 | for (const plugins of config.passes) { | |
| 37 | for (const plugin of plugins) { | |
| 38 | if (plugin.manipulateOptions) { | |
| 39 | plugin.manipulateOptions(options, options.parserOpts); | |
| 40 | } | |
| 41 | } | |
| 42 | } | |
| 43 | return options; | |
| 44 | } |
src/file-viewer/highlight-grammar/css.plist created+53| ... | ... | @@ -0,0 +1,53 @@ |
| 1 | // default | |
| 2 | function* normalizeFile(pluginPasses, options, code, ast) { | |
| 3 | code = `${code || ""}`; | |
| 4 | if (ast) { | |
| 5 | if (ast.type === "Program") { | |
| 6 | ast = file(ast, [], []); | |
| 7 | } else if (ast.type !== "File") { | |
| 8 | throw new Error("AST root must be a Program or File node"); | |
| 9 | } | |
| 10 | if (options.cloneInputAst) { | |
| 11 | ast = (0, _cloneDeep.default)(ast); | |
| 12 | } | |
| 13 | } else { | |
| 14 | ast = yield* (0, _index.default)(pluginPasses, options, code); | |
| 15 | } | |
| 16 | let inputMap = null; | |
| 17 | if (options.inputSourceMap !== false) { | |
| 18 | if (typeof options.inputSourceMap === "object") { | |
| 19 | inputMap = _convertSourceMap().fromObject(options.inputSourceMap); | |
| 20 | } | |
| 21 | if (!inputMap) { | |
| 22 | const lastComment = extractComments(INLINE_SOURCEMAP_REGEX, ast); | |
| 23 | if (lastComment) { | |
| 24 | try { | |
| 25 | inputMap = _convertSourceMap().fromComment("//" + lastComment); | |
| 26 | } catch (err) { | |
| 27 | { | |
| 28 | debug("discarding unknown inline input sourcemap"); | |
| 29 | } | |
| 30 | } | |
| 31 | } | |
| 32 | } | |
| 33 | if (!inputMap) { | |
| 34 | const lastComment = extractComments(EXTERNAL_SOURCEMAP_REGEX, ast); | |
| 35 | if (typeof options.filename === "string" && lastComment) { | |
| 36 | try { | |
| 37 | const match = EXTERNAL_SOURCEMAP_REGEX.exec(lastComment); | |
| 38 | const inputMapContent = _fs().readFileSync(_path().resolve(_path().dirname(options.filename), match[1]), "utf8"); | |
| 39 | inputMap = _convertSourceMap().fromJSON(inputMapContent); | |
| 40 | } catch (err) { | |
| 41 | debug("discarding unknown file input sourcemap", err); | |
| 42 | } | |
| 43 | } else if (lastComment) { | |
| 44 | debug("discarding un-loadable file input sourcemap"); | |
| 45 | } | |
| 46 | } | |
| 47 | } | |
| 48 | return new _file.default(options, { | |
| 49 | code, | |
| 50 | ast: ast, | |
| 51 | inputMap | |
| 52 | }); | |
| 53 | } |
src/file-viewer/highlight-grammar/diff.plist created+56| ... | ... | @@ -0,0 +1,56 @@ |
| 1 | // default | |
| 2 | function* parser(pluginPasses, { | |
| 3 | parserOpts, | |
| 4 | highlightCode = true, | |
| 5 | filename = "unknown" | |
| 6 | }, code) { | |
| 7 | try { | |
| 8 | const results = []; | |
| 9 | for (const plugins of pluginPasses) { | |
| 10 | for (const plugin of plugins) { | |
| 11 | const { | |
| 12 | parserOverride | |
| 13 | } = plugin; | |
| 14 | if (parserOverride) { | |
| 15 | const ast = parserOverride(code, parserOpts, _parser().parse); | |
| 16 | if (ast !== undefined) results.push(ast); | |
| 17 | } | |
| 18 | } | |
| 19 | } | |
| 20 | if (results.length === 0) { | |
| 21 | return (0, _parser().parse)(code, parserOpts); | |
| 22 | } else if (results.length === 1) { | |
| 23 | yield* []; | |
| 24 | if (typeof results[0].then === "function") { | |
| 25 | throw new Error(`You appear to be using an async parser plugin, ` + `which your current version of Babel does not support. ` + `If you're using a published plugin, you may need to upgrade ` + `your @babel/core version.`); | |
| 26 | } | |
| 27 | return results[0]; | |
| 28 | } | |
| 29 | throw new Error("More than one plugin attempted to override parsing."); | |
| 30 | } catch (err) { | |
| 31 | if (err.code === "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED") { | |
| 32 | err.message += "\nConsider renaming the file to '.mjs', or setting sourceType:module " + "or sourceType:unambiguous in your Babel config for this file."; | |
| 33 | } | |
| 34 | const { | |
| 35 | loc, | |
| 36 | missingPlugin | |
| 37 | } = err; | |
| 38 | if (loc) { | |
| 39 | const codeFrame = (0, _codeFrame().codeFrameColumns)(code, { | |
| 40 | start: { | |
| 41 | line: loc.line, | |
| 42 | column: loc.column + 1 | |
| 43 | } | |
| 44 | }, { | |
| 45 | highlightCode | |
| 46 | }); | |
| 47 | if (missingPlugin) { | |
| 48 | err.message = `${filename}: ` + (0, _missingPluginHelper.default)(missingPlugin[0], loc, codeFrame, filename); | |
| 49 | } else { | |
| 50 | err.message = `${filename}: ${err.message}\n\n` + codeFrame; | |
| 51 | } | |
| 52 | err.code = "BABEL_PARSE_ERROR"; | |
| 53 | } | |
| 54 | throw err; | |
| 55 | } | |
| 56 | } |
src/file-viewer/highlight-grammar/dosbatch.plist created+33| ... | ... | @@ -0,0 +1,33 @@ |
| 1 | // default | |
| 2 | function generateMissingPluginMessage(missingPluginName, loc, codeFrame, filename) { | |
| 3 | let helpMessage = `Support for the experimental syntax '${missingPluginName}' isn't currently enabled ` + `(${loc.line}:${loc.column + 1}):\n\n` + codeFrame; | |
| 4 | const pluginInfo = pluginNameMap[missingPluginName]; | |
| 5 | if (pluginInfo) { | |
| 6 | const { | |
| 7 | syntax: syntaxPlugin, | |
| 8 | transform: transformPlugin | |
| 9 | } = pluginInfo; | |
| 10 | if (syntaxPlugin) { | |
| 11 | const syntaxPluginInfo = getNameURLCombination(syntaxPlugin); | |
| 12 | if (transformPlugin) { | |
| 13 | const transformPluginInfo = getNameURLCombination(transformPlugin); | |
| 14 | const sectionType = transformPlugin.name.startsWith("@babel/plugin") ? "plugins" : "presets"; | |
| 15 | helpMessage += `\n\nAdd ${transformPluginInfo} to the '${sectionType}' section of your Babel config to enable transformation. | |
| 16 | If you want to leave it as-is, add ${syntaxPluginInfo} to the 'plugins' section to enable parsing.`; | |
| 17 | } else { | |
| 18 | helpMessage += `\n\nAdd ${syntaxPluginInfo} to the 'plugins' section of your Babel config ` + `to enable parsing.`; | |
| 19 | } | |
| 20 | } | |
| 21 | } | |
| 22 | const msgFilename = filename === "unknown" ? "<name of the input file>" : filename; | |
| 23 | helpMessage += ` | |
| 24 | ||
| 25 | If you already added the plugin for this syntax to your config, it's possible that your config \ | |
| 26 | isn't being loaded. | |
| 27 | You can re-run Babel with the BABEL_SHOW_CONFIG_FOR environment variable to show the loaded \ | |
| 28 | configuration: | |
| 29 | \tnpx cross-env BABEL_SHOW_CONFIG_FOR=${msgFilename} <your build command> | |
| 30 | See https://babeljs.io/docs/configuration#print-effective-configs for more info. | |
| 31 | `; | |
| 32 | return helpMessage; | |
| 33 | } |
src/file-viewer/highlight-grammar/json.plist created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | // default | |
| 2 | function _default(value) { | |
| 3 | if (typeof value !== "object") return value; | |
| 4 | { | |
| 5 | try { | |
| 6 | return deepClone(value, new Map(), true); | |
| 7 | } catch (_) { | |
| 8 | return structuredClone(value); | |
| 9 | } | |
| 10 | } | |
| 11 | } |
src/file-viewer/highlight-grammar/lua.plist created+61| ... | ... | @@ -0,0 +1,61 @@ |
| 1 | // default | |
| 2 | function generateCode(pluginPasses, file) { | |
| 3 | const { | |
| 4 | opts, | |
| 5 | ast, | |
| 6 | code, | |
| 7 | inputMap | |
| 8 | } = file; | |
| 9 | const { | |
| 10 | generatorOpts | |
| 11 | } = opts; | |
| 12 | generatorOpts.inputSourceMap = inputMap == null ? void 0 : inputMap.toObject(); | |
| 13 | const results = []; | |
| 14 | for (const plugins of pluginPasses) { | |
| 15 | for (const plugin of plugins) { | |
| 16 | const { | |
| 17 | generatorOverride | |
| 18 | } = plugin; | |
| 19 | if (generatorOverride) { | |
| 20 | const result = generatorOverride(ast, generatorOpts, code, _generator().default); | |
| 21 | if (result !== undefined) results.push(result); | |
| 22 | } | |
| 23 | } | |
| 24 | } | |
| 25 | let result; | |
| 26 | if (results.length === 0) { | |
| 27 | result = (0, _generator().default)(ast, generatorOpts, code); | |
| 28 | } else if (results.length === 1) { | |
| 29 | result = results[0]; | |
| 30 | if (typeof result.then === "function") { | |
| 31 | throw new Error(`You appear to be using an async codegen plugin, ` + `which your current version of Babel does not support. ` + `If you're using a published plugin, ` + `you may need to upgrade your @babel/core version.`); | |
| 32 | } | |
| 33 | } else { | |
| 34 | throw new Error("More than one plugin attempted to override codegen."); | |
| 35 | } | |
| 36 | let { | |
| 37 | code: outputCode, | |
| 38 | decodedMap: outputMap = result.map | |
| 39 | } = result; | |
| 40 | if (result.__mergedMap) { | |
| 41 | outputMap = Object.assign({}, result.map); | |
| 42 | } else { | |
| 43 | if (outputMap) { | |
| 44 | if (inputMap) { | |
| 45 | outputMap = (0, _mergeMap.default)(inputMap.toObject(), outputMap, generatorOpts.sourceFileName); | |
| 46 | } else { | |
| 47 | outputMap = result.map; | |
| 48 | } | |
| 49 | } | |
| 50 | } | |
| 51 | if (opts.sourceMaps === "inline" || opts.sourceMaps === "both") { | |
| 52 | outputCode += "\n" + _convertSourceMap().fromObject(outputMap).toComment(); | |
| 53 | } | |
| 54 | if (opts.sourceMaps === "inline") { | |
| 55 | outputMap = null; | |
| 56 | } | |
| 57 | return { | |
| 58 | outputCode, | |
| 59 | outputMap | |
| 60 | }; | |
| 61 | } |
src/file-viewer/highlight-grammar/mdx.plist created+17| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | // default | |
| 2 | function mergeSourceMap(inputMap, map, sourceFileName) { | |
| 3 | const source = sourceFileName.replace(/\\/g, "/"); | |
| 4 | let found = false; | |
| 5 | const result = _remapping()(rootless(map), (s, ctx) => { | |
| 6 | if (s === source && !found) { | |
| 7 | found = true; | |
| 8 | ctx.source = ""; | |
| 9 | return rootless(inputMap); | |
| 10 | } | |
| 11 | return null; | |
| 12 | }); | |
| 13 | if (typeof inputMap.sourceRoot === "string") { | |
| 14 | result.sourceRoot = inputMap.sourceRoot; | |
| 15 | } | |
| 16 | return Object.assign({}, result); | |
| 17 | } |
src/file-viewer/highlight-grammar/php.plist createdsrc/file-viewer/highlight-grammar/powershell.plist created+676| ... | ... | @@ -0,0 +1,676 @@ |
| 1 | // SEMVER_SPEC_VERSION | |
| 2 | 2.0.0 | |
| 3 | ||
| 4 | // re | |
| 5 | /0|[1-9]\d*/,/\d+/,/\d*[a-zA-Z-][a-zA-Z0-9-]*/,/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/,/(\d+)\.(\d+)\.(\d+)/,/(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)/,/(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)/,/(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))/,/(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))/,/[a-zA-Z0-9-]+/,/(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))/,/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?$/,/v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?/,/[v=\s]*(\d+)\.(\d+)\.(\d+)(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?/,/^[v=\s]*(\d+)\.(\d+)\.(\d+)(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?$/,/((?:<|>)?=?)/,/\d+|x|X|\*/,/0|[1-9]\d*|x|X|\*/,/[v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?/,/[v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?/,/^((?:<|>)?=?)\s*[v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$/,/^((?:<|>)?=?)\s*[v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$/,/(^|[^\d])(\d{1,16})(?:\.(\d{1,16}))?(?:\.(\d{1,16}))?(?:$|[^\d])/,/(^|[^\d])(\d{1,16})(?:\.(\d{1,16}))?(?:\.(\d{1,16}))?(?:$|[^\d])/g,/(?:~>?)/,/(\s*)(?:~>?)\s+/g,/^(?:~>?)[v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$/,/^(?:~>?)[v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$/,/(?:\^)/,/(\s*)(?:\^)\s+/g,/^(?:\^)[v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$/,/^(?:\^)[v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$/,/^((?:<|>)?=?)\s*([v=\s]*(\d+)\.(\d+)\.(\d+)(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)$|^$/,/^((?:<|>)?=?)\s*(v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)$|^$/,/(\s*)((?:<|>)?=?)\s*([v=\s]*(\d+)\.(\d+)\.(\d+)(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?|[v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?)/g,/^\s*([v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?)\s+-\s+([v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?)\s*$/,/^\s*([v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?)\s+-\s+([v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?)\s*$/,/(<|>)?=?\s*\*/ | |
| 6 | ||
| 7 | // safeRe | |
| 8 | /0|[1-9]\d{0,256}/,/\d{1,256}/,/\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}/,/(0|[1-9]\d{0,256})\.(0|[1-9]\d{0,256})\.(0|[1-9]\d{0,256})/,/(\d{1,256})\.(\d{1,256})\.(\d{1,256})/,/(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})/,/(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})/,/(?:-((?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*))/,/(?:-?((?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*))/,/[a-zA-Z0-9-]{1,250}/,/(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))/,/^v?(0|[1-9]\d{0,256})\.(0|[1-9]\d{0,256})\.(0|[1-9]\d{0,256})(?:-((?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?$/,/v?(0|[1-9]\d{0,256})\.(0|[1-9]\d{0,256})\.(0|[1-9]\d{0,256})(?:-((?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?/,/[v=\s]*(\d{1,256})\.(\d{1,256})\.(\d{1,256})(?:-?((?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?/,/^[v=\s]*(\d{1,256})\.(\d{1,256})\.(\d{1,256})(?:-?((?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?$/,/((?:<|>)?=?)/,/\d{1,256}|x|X|\*/,/0|[1-9]\d{0,256}|x|X|\*/,/[v=\s]*(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:(?:-((?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?/,/[v=\s]*(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:(?:-?((?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?/,/^((?:<|>)?=?)\s{0,1}[v=\s]*(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:(?:-((?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?$/,/^((?:<|>)?=?)\s{0,1}[v=\s]*(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:(?:-?((?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?$/,/(^|[^\d])(\d{1,16})(?:\.(\d{1,16}))?(?:\.(\d{1,16}))?(?:$|[^\d])/,/(^|[^\d])(\d{1,16})(?:\.(\d{1,16}))?(?:\.(\d{1,16}))?(?:$|[^\d])/g,/(?:~>?)/,/(\s{0,1})(?:~>?)\s{1,1}/g,/^(?:~>?)[v=\s]*(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:(?:-((?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?$/,/^(?:~>?)[v=\s]*(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:(?:-?((?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?$/,/(?:\^)/,/(\s{0,1})(?:\^)\s{1,1}/g,/^(?:\^)[v=\s]*(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:(?:-((?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?$/,/^(?:\^)[v=\s]*(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:(?:-?((?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?$/,/^((?:<|>)?=?)\s{0,1}([v=\s]*(\d{1,256})\.(\d{1,256})\.(\d{1,256})(?:-?((?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)$|^$/,/^((?:<|>)?=?)\s{0,1}(v?(0|[1-9]\d{0,256})\.(0|[1-9]\d{0,256})\.(0|[1-9]\d{0,256})(?:-((?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)$|^$/,/(\s{0,1})((?:<|>)?=?)\s{0,1}([v=\s]*(\d{1,256})\.(\d{1,256})\.(\d{1,256})(?:-?((?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?|[v=\s]*(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:(?:-((?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?)/g,/^\s{0,1}([v=\s]*(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:(?:-((?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?)\s{1,1}-\s{1,1}([v=\s]*(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:\.(0|[1-9]\d{0,256}|x|X|\*)(?:(?:-((?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:0|[1-9]\d{0,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?)\s{0,1}$/,/^\s{0,1}([v=\s]*(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:(?:-?((?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?)\s{1,1}-\s{1,1}([v=\s]*(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:\.(\d{1,256}|x|X|\*)(?:(?:-?((?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250})(?:\.(?:\d{1,256}|\d{0,256}[a-zA-Z-][a-zA-Z0-9-]{0,250}))*)))?(?:\+([a-zA-Z0-9-]{1,250}(?:\.[a-zA-Z0-9-]{1,250})*))?)?)?)\s{0,1}$/,/(<|>)?=?\s{0,1}\*/ | |
| 9 | ||
| 10 | // src | |
| 11 | 0|[1-9]\d*,\d+,\d*[a-zA-Z-][a-zA-Z0-9-]*,(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*),(\d+)\.(\d+)\.(\d+),(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*),(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*),(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)),(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)),[a-zA-Z0-9-]+,(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)),^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?$,v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?,[v=\s]*(\d+)\.(\d+)\.(\d+)(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?,^[v=\s]*(\d+)\.(\d+)\.(\d+)(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?$,((?:<|>)?=?),\d+|x|X|\*,0|[1-9]\d*|x|X|\*,[v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?,[v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?,^((?:<|>)?=?)\s*[v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$,^((?:<|>)?=?)\s*[v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$,(^|[^\d])(\d{1,16})(?:\.(\d{1,16}))?(?:\.(\d{1,16}))?(?:$|[^\d]),,(?:~>?),(\s*)(?:~>?)\s+,^(?:~>?)[v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$,^(?:~>?)[v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$,(?:\^),(\s*)(?:\^)\s+,^(?:\^)[v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$,^(?:\^)[v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?$,^((?:<|>)?=?)\s*([v=\s]*(\d+)\.(\d+)\.(\d+)(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)$|^$,^((?:<|>)?=?)\s*(v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)$|^$,(\s*)((?:<|>)?=?)\s*([v=\s]*(\d+)\.(\d+)\.(\d+)(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?|[v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?),^\s*([v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?)\s+-\s+([v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?)\s*$,^\s*([v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?)\s+-\s+([v=\s]*(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:\.(\d+|x|X|\*)(?:(?:-?((?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:\d+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?)?)?)\s*$,(<|>)?=?\s*\* | |
| 12 | ||
| 13 | // tokens | |
| 14 | [object Object] | |
| 15 | ||
| 16 | // parse | |
| 17 | function parse (version, options) { | |
| 18 | if (!options || typeof options !== 'object') { | |
| 19 | options = { | |
| 20 | loose: !!options, | |
| 21 | includePrerelease: false | |
| 22 | } | |
| 23 | } | |
| 24 | ||
| 25 | if (version instanceof SemVer) { | |
| 26 | return version | |
| 27 | } | |
| 28 | ||
| 29 | if (typeof version !== 'string') { | |
| 30 | return null | |
| 31 | } | |
| 32 | ||
| 33 | if (version.length > MAX_LENGTH) { | |
| 34 | return null | |
| 35 | } | |
| 36 | ||
| 37 | var r = options.loose ? safeRe[t.LOOSE] : safeRe[t.FULL] | |
| 38 | if (!r.test(version)) { | |
| 39 | return null | |
| 40 | } | |
| 41 | ||
| 42 | try { | |
| 43 | return new SemVer(version, options) | |
| 44 | } catch (er) { | |
| 45 | return null | |
| 46 | } | |
| 47 | } | |
| 48 | ||
| 49 | // valid | |
| 50 | function valid (version, options) { | |
| 51 | var v = parse(version, options) | |
| 52 | return v ? v.version : null | |
| 53 | } | |
| 54 | ||
| 55 | // clean | |
| 56 | function clean (version, options) { | |
| 57 | var s = parse(version.trim().replace(/^[=v]+/, ''), options) | |
| 58 | return s ? s.version : null | |
| 59 | } | |
| 60 | ||
| 61 | // SemVer | |
| 62 | function SemVer (version, options) { | |
| 63 | if (!options || typeof options !== 'object') { | |
| 64 | options = { | |
| 65 | loose: !!options, | |
| 66 | includePrerelease: false | |
| 67 | } | |
| 68 | } | |
| 69 | if (version instanceof SemVer) { | |
| 70 | if (version.loose === options.loose) { | |
| 71 | return version | |
| 72 | } else { | |
| 73 | version = version.version | |
| 74 | } | |
| 75 | } else if (typeof version !== 'string') { | |
| 76 | throw new TypeError('Invalid Version: ' + version) | |
| 77 | } | |
| 78 | ||
| 79 | if (version.length > MAX_LENGTH) { | |
| 80 | throw new TypeError('version is longer than ' + MAX_LENGTH + ' characters') | |
| 81 | } | |
| 82 | ||
| 83 | if (!(this instanceof SemVer)) { | |
| 84 | return new SemVer(version, options) | |
| 85 | } | |
| 86 | ||
| 87 | debug('SemVer', version, options) | |
| 88 | this.options = options | |
| 89 | this.loose = !!options.loose | |
| 90 | ||
| 91 | var m = version.trim().match(options.loose ? safeRe[t.LOOSE] : safeRe[t.FULL]) | |
| 92 | ||
| 93 | if (!m) { | |
| 94 | throw new TypeError('Invalid Version: ' + version) | |
| 95 | } | |
| 96 | ||
| 97 | this.raw = version | |
| 98 | ||
| 99 | // these are actually numbers | |
| 100 | this.major = +m[1] | |
| 101 | this.minor = +m[2] | |
| 102 | this.patch = +m[3] | |
| 103 | ||
| 104 | if (this.major > MAX_SAFE_INTEGER || this.major < 0) { | |
| 105 | throw new TypeError('Invalid major version') | |
| 106 | } | |
| 107 | ||
| 108 | if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) { | |
| 109 | throw new TypeError('Invalid minor version') | |
| 110 | } | |
| 111 | ||
| 112 | if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) { | |
| 113 | throw new TypeError('Invalid patch version') | |
| 114 | } | |
| 115 | ||
| 116 | // numberify any prerelease numeric ids | |
| 117 | if (!m[4]) { | |
| 118 | this.prerelease = [] | |
| 119 | } else { | |
| 120 | this.prerelease = m[4].split('.').map(function (id) { | |
| 121 | if (/^[0-9]+$/.test(id)) { | |
| 122 | var num = +id | |
| 123 | if (num >= 0 && num < MAX_SAFE_INTEGER) { | |
| 124 | return num | |
| 125 | } | |
| 126 | } | |
| 127 | return id | |
| 128 | }) | |
| 129 | } | |
| 130 | ||
| 131 | this.build = m[5] ? m[5].split('.') : [] | |
| 132 | this.format() | |
| 133 | } | |
| 134 | ||
| 135 | // inc | |
| 136 | function inc (version, release, loose, identifier) { | |
| 137 | if (typeof (loose) === 'string') { | |
| 138 | identifier = loose | |
| 139 | loose = undefined | |
| 140 | } | |
| 141 | ||
| 142 | try { | |
| 143 | return new SemVer(version, loose).inc(release, identifier).version | |
| 144 | } catch (er) { | |
| 145 | return null | |
| 146 | } | |
| 147 | } | |
| 148 | ||
| 149 | // diff | |
| 150 | function diff (version1, version2) { | |
| 151 | if (eq(version1, version2)) { | |
| 152 | return null | |
| 153 | } else { | |
| 154 | var v1 = parse(version1) | |
| 155 | var v2 = parse(version2) | |
| 156 | var prefix = '' | |
| 157 | if (v1.prerelease.length || v2.prerelease.length) { | |
| 158 | prefix = 'pre' | |
| 159 | var defaultResult = 'prerelease' | |
| 160 | } | |
| 161 | for (var key in v1) { | |
| 162 | if (key === 'major' || key === 'minor' || key === 'patch') { | |
| 163 | if (v1[key] !== v2[key]) { | |
| 164 | return prefix + key | |
| 165 | } | |
| 166 | } | |
| 167 | } | |
| 168 | return defaultResult // may be undefined | |
| 169 | } | |
| 170 | } | |
| 171 | ||
| 172 | // compareIdentifiers | |
| 173 | function compareIdentifiers (a, b) { | |
| 174 | var anum = numeric.test(a) | |
| 175 | var bnum = numeric.test(b) | |
| 176 | ||
| 177 | if (anum && bnum) { | |
| 178 | a = +a | |
| 179 | b = +b | |
| 180 | } | |
| 181 | ||
| 182 | return a === b ? 0 | |
| 183 | : (anum && !bnum) ? -1 | |
| 184 | : (bnum && !anum) ? 1 | |
| 185 | : a < b ? -1 | |
| 186 | : 1 | |
| 187 | } | |
| 188 | ||
| 189 | // rcompareIdentifiers | |
| 190 | function rcompareIdentifiers (a, b) { | |
| 191 | return compareIdentifiers(b, a) | |
| 192 | } | |
| 193 | ||
| 194 | // major | |
| 195 | function major (a, loose) { | |
| 196 | return new SemVer(a, loose).major | |
| 197 | } | |
| 198 | ||
| 199 | // minor | |
| 200 | function minor (a, loose) { | |
| 201 | return new SemVer(a, loose).minor | |
| 202 | } | |
| 203 | ||
| 204 | // patch | |
| 205 | function patch (a, loose) { | |
| 206 | return new SemVer(a, loose).patch | |
| 207 | } | |
| 208 | ||
| 209 | // compare | |
| 210 | function compare (a, b, loose) { | |
| 211 | return new SemVer(a, loose).compare(new SemVer(b, loose)) | |
| 212 | } | |
| 213 | ||
| 214 | // compareLoose | |
| 215 | function compareLoose (a, b) { | |
| 216 | return compare(a, b, true) | |
| 217 | } | |
| 218 | ||
| 219 | // compareBuild | |
| 220 | function compareBuild (a, b, loose) { | |
| 221 | var versionA = new SemVer(a, loose) | |
| 222 | var versionB = new SemVer(b, loose) | |
| 223 | return versionA.compare(versionB) || versionA.compareBuild(versionB) | |
| 224 | } | |
| 225 | ||
| 226 | // rcompare | |
| 227 | function rcompare (a, b, loose) { | |
| 228 | return compare(b, a, loose) | |
| 229 | } | |
| 230 | ||
| 231 | // sort | |
| 232 | function sort (list, loose) { | |
| 233 | return list.sort(function (a, b) { | |
| 234 | return exports.compareBuild(a, b, loose) | |
| 235 | }) | |
| 236 | } | |
| 237 | ||
| 238 | // rsort | |
| 239 | function rsort (list, loose) { | |
| 240 | return list.sort(function (a, b) { | |
| 241 | return exports.compareBuild(b, a, loose) | |
| 242 | }) | |
| 243 | } | |
| 244 | ||
| 245 | // gt | |
| 246 | function gt (a, b, loose) { | |
| 247 | return compare(a, b, loose) > 0 | |
| 248 | } | |
| 249 | ||
| 250 | // lt | |
| 251 | function lt (a, b, loose) { | |
| 252 | return compare(a, b, loose) < 0 | |
| 253 | } | |
| 254 | ||
| 255 | // eq | |
| 256 | function eq (a, b, loose) { | |
| 257 | return compare(a, b, loose) === 0 | |
| 258 | } | |
| 259 | ||
| 260 | // neq | |
| 261 | function neq (a, b, loose) { | |
| 262 | return compare(a, b, loose) !== 0 | |
| 263 | } | |
| 264 | ||
| 265 | // gte | |
| 266 | function gte (a, b, loose) { | |
| 267 | return compare(a, b, loose) >= 0 | |
| 268 | } | |
| 269 | ||
| 270 | // lte | |
| 271 | function lte (a, b, loose) { | |
| 272 | return compare(a, b, loose) <= 0 | |
| 273 | } | |
| 274 | ||
| 275 | // cmp | |
| 276 | function cmp (a, op, b, loose) { | |
| 277 | switch (op) { | |
| 278 | case '===': | |
| 279 | if (typeof a === 'object') | |
| 280 | a = a.version | |
| 281 | if (typeof b === 'object') | |
| 282 | b = b.version | |
| 283 | return a === b | |
| 284 | ||
| 285 | case '!==': | |
| 286 | if (typeof a === 'object') | |
| 287 | a = a.version | |
| 288 | if (typeof b === 'object') | |
| 289 | b = b.version | |
| 290 | return a !== b | |
| 291 | ||
| 292 | case '': | |
| 293 | case '=': | |
| 294 | case '==': | |
| 295 | return eq(a, b, loose) | |
| 296 | ||
| 297 | case '!=': | |
| 298 | return neq(a, b, loose) | |
| 299 | ||
| 300 | case '>': | |
| 301 | return gt(a, b, loose) | |
| 302 | ||
| 303 | case '>=': | |
| 304 | return gte(a, b, loose) | |
| 305 | ||
| 306 | case '<': | |
| 307 | return lt(a, b, loose) | |
| 308 | ||
| 309 | case '<=': | |
| 310 | return lte(a, b, loose) | |
| 311 | ||
| 312 | default: | |
| 313 | throw new TypeError('Invalid operator: ' + op) | |
| 314 | } | |
| 315 | } | |
| 316 | ||
| 317 | // Comparator | |
| 318 | function Comparator (comp, options) { | |
| 319 | if (!options || typeof options !== 'object') { | |
| 320 | options = { | |
| 321 | loose: !!options, | |
| 322 | includePrerelease: false | |
| 323 | } | |
| 324 | } | |
| 325 | ||
| 326 | if (comp instanceof Comparator) { | |
| 327 | if (comp.loose === !!options.loose) { | |
| 328 | return comp | |
| 329 | } else { | |
| 330 | comp = comp.value | |
| 331 | } | |
| 332 | } | |
| 333 | ||
| 334 | if (!(this instanceof Comparator)) { | |
| 335 | return new Comparator(comp, options) | |
| 336 | } | |
| 337 | ||
| 338 | comp = comp.trim().split(/\s+/).join(' ') | |
| 339 | debug('comparator', comp, options) | |
| 340 | this.options = options | |
| 341 | this.loose = !!options.loose | |
| 342 | this.parse(comp) | |
| 343 | ||
| 344 | if (this.semver === ANY) { | |
| 345 | this.value = '' | |
| 346 | } else { | |
| 347 | this.value = this.operator + this.semver.version | |
| 348 | } | |
| 349 | ||
| 350 | debug('comp', this) | |
| 351 | } | |
| 352 | ||
| 353 | // Range | |
| 354 | function Range (range, options) { | |
| 355 | if (!options || typeof options !== 'object') { | |
| 356 | options = { | |
| 357 | loose: !!options, | |
| 358 | includePrerelease: false | |
| 359 | } | |
| 360 | } | |
| 361 | ||
| 362 | if (range instanceof Range) { | |
| 363 | if (range.loose === !!options.loose && | |
| 364 | range.includePrerelease === !!options.includePrerelease) { | |
| 365 | return range | |
| 366 | } else { | |
| 367 | return new Range(range.raw, options) | |
| 368 | } | |
| 369 | } | |
| 370 | ||
| 371 | if (range instanceof Comparator) { | |
| 372 | return new Range(range.value, options) | |
| 373 | } | |
| 374 | ||
| 375 | if (!(this instanceof Range)) { | |
| 376 | return new Range(range, options) | |
| 377 | } | |
| 378 | ||
| 379 | this.options = options | |
| 380 | this.loose = !!options.loose | |
| 381 | this.includePrerelease = !!options.includePrerelease | |
| 382 | ||
| 383 | // First reduce all whitespace as much as possible so we do not have to rely | |
| 384 | // on potentially slow regexes like \s*. This is then stored and used for | |
| 385 | // future error messages as well. | |
| 386 | this.raw = range | |
| 387 | .trim() | |
| 388 | .split(/\s+/) | |
| 389 | .join(' ') | |
| 390 | ||
| 391 | // First, split based on boolean or || | |
| 392 | this.set = this.raw.split('||').map(function (range) { | |
| 393 | return this.parseRange(range.trim()) | |
| 394 | }, this).filter(function (c) { | |
| 395 | // throw out any that are not relevant for whatever reason | |
| 396 | return c.length | |
| 397 | }) | |
| 398 | ||
| 399 | if (!this.set.length) { | |
| 400 | throw new TypeError('Invalid SemVer Range: ' + this.raw) | |
| 401 | } | |
| 402 | ||
| 403 | this.format() | |
| 404 | } | |
| 405 | ||
| 406 | // toComparators | |
| 407 | function toComparators (range, options) { | |
| 408 | return new Range(range, options).set.map(function (comp) { | |
| 409 | return comp.map(function (c) { | |
| 410 | return c.value | |
| 411 | }).join(' ').trim().split(' ') | |
| 412 | }) | |
| 413 | } | |
| 414 | ||
| 415 | // satisfies | |
| 416 | function satisfies (version, range, options) { | |
| 417 | try { | |
| 418 | range = new Range(range, options) | |
| 419 | } catch (er) { | |
| 420 | return false | |
| 421 | } | |
| 422 | return range.test(version) | |
| 423 | } | |
| 424 | ||
| 425 | // maxSatisfying | |
| 426 | function maxSatisfying (versions, range, options) { | |
| 427 | var max = null | |
| 428 | var maxSV = null | |
| 429 | try { | |
| 430 | var rangeObj = new Range(range, options) | |
| 431 | } catch (er) { | |
| 432 | return null | |
| 433 | } | |
| 434 | versions.forEach(function (v) { | |
| 435 | if (rangeObj.test(v)) { | |
| 436 | // satisfies(v, range, options) | |
| 437 | if (!max || maxSV.compare(v) === -1) { | |
| 438 | // compare(max, v, true) | |
| 439 | max = v | |
| 440 | maxSV = new SemVer(max, options) | |
| 441 | } | |
| 442 | } | |
| 443 | }) | |
| 444 | return max | |
| 445 | } | |
| 446 | ||
| 447 | // minSatisfying | |
| 448 | function minSatisfying (versions, range, options) { | |
| 449 | var min = null | |
| 450 | var minSV = null | |
| 451 | try { | |
| 452 | var rangeObj = new Range(range, options) | |
| 453 | } catch (er) { | |
| 454 | return null | |
| 455 | } | |
| 456 | versions.forEach(function (v) { | |
| 457 | if (rangeObj.test(v)) { | |
| 458 | // satisfies(v, range, options) | |
| 459 | if (!min || minSV.compare(v) === 1) { | |
| 460 | // compare(min, v, true) | |
| 461 | min = v | |
| 462 | minSV = new SemVer(min, options) | |
| 463 | } | |
| 464 | } | |
| 465 | }) | |
| 466 | return min | |
| 467 | } | |
| 468 | ||
| 469 | // minVersion | |
| 470 | function minVersion (range, loose) { | |
| 471 | range = new Range(range, loose) | |
| 472 | ||
| 473 | var minver = new SemVer('0.0.0') | |
| 474 | if (range.test(minver)) { | |
| 475 | return minver | |
| 476 | } | |
| 477 | ||
| 478 | minver = new SemVer('0.0.0-0') | |
| 479 | if (range.test(minver)) { | |
| 480 | return minver | |
| 481 | } | |
| 482 | ||
| 483 | minver = null | |
| 484 | for (var i = 0; i < range.set.length; ++i) { | |
| 485 | var comparators = range.set[i] | |
| 486 | ||
| 487 | comparators.forEach(function (comparator) { | |
| 488 | // Clone to avoid manipulating the comparator's semver object. | |
| 489 | var compver = new SemVer(comparator.semver.version) | |
| 490 | switch (comparator.operator) { | |
| 491 | case '>': | |
| 492 | if (compver.prerelease.length === 0) { | |
| 493 | compver.patch++ | |
| 494 | } else { | |
| 495 | compver.prerelease.push(0) | |
| 496 | } | |
| 497 | compver.raw = compver.format() | |
| 498 | /* fallthrough */ | |
| 499 | case '': | |
| 500 | case '>=': | |
| 501 | if (!minver || gt(minver, compver)) { | |
| 502 | minver = compver | |
| 503 | } | |
| 504 | break | |
| 505 | case '<': | |
| 506 | case '<=': | |
| 507 | /* Ignore maximum versions */ | |
| 508 | break | |
| 509 | /* istanbul ignore next */ | |
| 510 | default: | |
| 511 | throw new Error('Unexpected operation: ' + comparator.operator) | |
| 512 | } | |
| 513 | }) | |
| 514 | } | |
| 515 | ||
| 516 | if (minver && range.test(minver)) { | |
| 517 | return minver | |
| 518 | } | |
| 519 | ||
| 520 | return null | |
| 521 | } | |
| 522 | ||
| 523 | // validRange | |
| 524 | function validRange (range, options) { | |
| 525 | try { | |
| 526 | // Return '*' instead of '' so that truthiness works. | |
| 527 | // This will throw if it's invalid anyway | |
| 528 | return new Range(range, options).range || '*' | |
| 529 | } catch (er) { | |
| 530 | return null | |
| 531 | } | |
| 532 | } | |
| 533 | ||
| 534 | // ltr | |
| 535 | function ltr (version, range, options) { | |
| 536 | return outside(version, range, '<', options) | |
| 537 | } | |
| 538 | ||
| 539 | // gtr | |
| 540 | function gtr (version, range, options) { | |
| 541 | return outside(version, range, '>', options) | |
| 542 | } | |
| 543 | ||
| 544 | // outside | |
| 545 | function outside (version, range, hilo, options) { | |
| 546 | version = new SemVer(version, options) | |
| 547 | range = new Range(range, options) | |
| 548 | ||
| 549 | var gtfn, ltefn, ltfn, comp, ecomp | |
| 550 | switch (hilo) { | |
| 551 | case '>': | |
| 552 | gtfn = gt | |
| 553 | ltefn = lte | |
| 554 | ltfn = lt | |
| 555 | comp = '>' | |
| 556 | ecomp = '>=' | |
| 557 | break | |
| 558 | case '<': | |
| 559 | gtfn = lt | |
| 560 | ltefn = gte | |
| 561 | ltfn = gt | |
| 562 | comp = '<' | |
| 563 | ecomp = '<=' | |
| 564 | break | |
| 565 | default: | |
| 566 | throw new TypeError('Must provide a hilo val of "<" or ">"') | |
| 567 | } | |
| 568 | ||
| 569 | // If it satisifes the range it is not outside | |
| 570 | if (satisfies(version, range, options)) { | |
| 571 | return false | |
| 572 | } | |
| 573 | ||
| 574 | // From now on, variable terms are as if we're in "gtr" mode. | |
| 575 | // but note that everything is flipped for the "ltr" function. | |
| 576 | ||
| 577 | for (var i = 0; i < range.set.length; ++i) { | |
| 578 | var comparators = range.set[i] | |
| 579 | ||
| 580 | var high = null | |
| 581 | var low = null | |
| 582 | ||
| 583 | comparators.forEach(function (comparator) { | |
| 584 | if (comparator.semver === ANY) { | |
| 585 | comparator = new Comparator('>=0.0.0') | |
| 586 | } | |
| 587 | high = high || comparator | |
| 588 | low = low || comparator | |
| 589 | if (gtfn(comparator.semver, high.semver, options)) { | |
| 590 | high = comparator | |
| 591 | } else if (ltfn(comparator.semver, low.semver, options)) { | |
| 592 | low = comparator | |
| 593 | } | |
| 594 | }) | |
| 595 | ||
| 596 | // If the edge version comparator has a operator then our version | |
| 597 | // isn't outside it | |
| 598 | if (high.operator === comp || high.operator === ecomp) { | |
| 599 | return false | |
| 600 | } | |
| 601 | ||
| 602 | // If the lowest version comparator has an operator and our version | |
| 603 | // is less than it then it isn't higher than the range | |
| 604 | if ((!low.operator || low.operator === comp) && | |
| 605 | ltefn(version, low.semver)) { | |
| 606 | return false | |
| 607 | } else if (low.operator === ecomp && ltfn(version, low.semver)) { | |
| 608 | return false | |
| 609 | } | |
| 610 | } | |
| 611 | return true | |
| 612 | } | |
| 613 | ||
| 614 | // prerelease | |
| 615 | function prerelease (version, options) { | |
| 616 | var parsed = parse(version, options) | |
| 617 | return (parsed && parsed.prerelease.length) ? parsed.prerelease : null | |
| 618 | } | |
| 619 | ||
| 620 | // intersects | |
| 621 | function intersects (r1, r2, options) { | |
| 622 | r1 = new Range(r1, options) | |
| 623 | r2 = new Range(r2, options) | |
| 624 | return r1.intersects(r2) | |
| 625 | } | |
| 626 | ||
| 627 | // coerce | |
| 628 | function coerce (version, options) { | |
| 629 | if (version instanceof SemVer) { | |
| 630 | return version | |
| 631 | } | |
| 632 | ||
| 633 | if (typeof version === 'number') { | |
| 634 | version = String(version) | |
| 635 | } | |
| 636 | ||
| 637 | if (typeof version !== 'string') { | |
| 638 | return null | |
| 639 | } | |
| 640 | ||
| 641 | options = options || {} | |
| 642 | ||
| 643 | var match = null | |
| 644 | if (!options.rtl) { | |
| 645 | match = version.match(safeRe[t.COERCE]) | |
| 646 | } else { | |
| 647 | // Find the right-most coercible string that does not share | |
| 648 | // a terminus with a more left-ward coercible string. | |
| 649 | // Eg, '1.2.3.4' wants to coerce '2.3.4', not '3.4' or '4' | |
| 650 | // | |
| 651 | // Walk through the string checking with a /g regexp | |
| 652 | // Manually set the index so as to pick up overlapping matches. | |
| 653 | // Stop when we get a match that ends at the string end, since no | |
| 654 | // coercible string can be more right-ward without the same terminus. | |
| 655 | var next | |
| 656 | while ((next = safeRe[t.COERCERTL].exec(version)) && | |
| 657 | (!match || match.index + match[0].length !== version.length) | |
| 658 | ) { | |
| 659 | if (!match || | |
| 660 | next.index + next[0].length !== match.index + match[0].length) { | |
| 661 | match = next | |
| 662 | } | |
| 663 | safeRe[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length | |
| 664 | } | |
| 665 | // leave it in a clean state | |
| 666 | safeRe[t.COERCERTL].lastIndex = -1 | |
| 667 | } | |
| 668 | ||
| 669 | if (match === null) { | |
| 670 | return null | |
| 671 | } | |
| 672 | ||
| 673 | return parse(match[2] + | |
| 674 | '.' + (match[3] || '0') + | |
| 675 | '.' + (match[4] || '0'), options) | |
| 676 | } |
src/file-viewer/highlight-grammar/python.plist created+33| ... | ... | @@ -0,0 +1,33 @@ |
| 1 | // loadPlugin | |
| 2 | function* loadPlugin(name, dirname) { | |
| 3 | const { | |
| 4 | filepath, | |
| 5 | loader | |
| 6 | } = resolvePlugin(name, dirname, yield* (0, _async.isAsync)()); | |
| 7 | const value = yield* requireModule("plugin", loader, filepath); | |
| 8 | debug("Loaded plugin %o from %o.", name, dirname); | |
| 9 | return { | |
| 10 | filepath, | |
| 11 | value | |
| 12 | }; | |
| 13 | } | |
| 14 | ||
| 15 | // loadPreset | |
| 16 | function* loadPreset(name, dirname) { | |
| 17 | const { | |
| 18 | filepath, | |
| 19 | loader | |
| 20 | } = resolvePreset(name, dirname, yield* (0, _async.isAsync)()); | |
| 21 | const value = yield* requireModule("preset", loader, filepath); | |
| 22 | debug("Loaded preset %o from %o.", name, dirname); | |
| 23 | return { | |
| 24 | filepath, | |
| 25 | value | |
| 26 | }; | |
| 27 | } | |
| 28 | ||
| 29 | // resolvePlugin | |
| 30 | function () { [native code] } | |
| 31 | ||
| 32 | // resolvePreset | |
| 33 | function () { [native code] } |
src/file-viewer/highlight-grammar/shell.plist created+52| ... | ... | @@ -0,0 +1,52 @@ |
| 1 | // moduleResolve | |
| 2 | function moduleResolve(specifier, base, conditions, preserveSymlinks) { | |
| 3 | const protocol = base.protocol; | |
| 4 | const isData = protocol === 'data:'; | |
| 5 | const isRemote = isData || protocol === 'http:' || protocol === 'https:'; | |
| 6 | let resolved; | |
| 7 | if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { | |
| 8 | try { | |
| 9 | resolved = new (_url().URL)(specifier, base); | |
| 10 | } catch (error_) { | |
| 11 | const error = new ERR_UNSUPPORTED_RESOLVE_REQUEST(specifier, base); | |
| 12 | error.cause = error_; | |
| 13 | throw error; | |
| 14 | } | |
| 15 | } else if (protocol === 'file:' && specifier[0] === '#') { | |
| 16 | resolved = packageImportsResolve(specifier, base, conditions); | |
| 17 | } else { | |
| 18 | try { | |
| 19 | resolved = new (_url().URL)(specifier); | |
| 20 | } catch (error_) { | |
| 21 | if (isRemote && !_module().builtinModules.includes(specifier)) { | |
| 22 | const error = new ERR_UNSUPPORTED_RESOLVE_REQUEST(specifier, base); | |
| 23 | error.cause = error_; | |
| 24 | throw error; | |
| 25 | } | |
| 26 | resolved = packageResolve(specifier, base, conditions); | |
| 27 | } | |
| 28 | } | |
| 29 | _assert()(resolved !== undefined, 'expected to be defined'); | |
| 30 | if (resolved.protocol !== 'file:') { | |
| 31 | return resolved; | |
| 32 | } | |
| 33 | return finalizeResolution(resolved, base, preserveSymlinks); | |
| 34 | } | |
| 35 | ||
| 36 | // resolve | |
| 37 | function resolve(specifier, parent) { | |
| 38 | if (!parent) { | |
| 39 | throw new Error('Please pass `parent`: `import-meta-resolve` cannot ponyfill that'); | |
| 40 | } | |
| 41 | try { | |
| 42 | return defaultResolve(specifier, { | |
| 43 | parentURL: parent | |
| 44 | }).url; | |
| 45 | } catch (error) { | |
| 46 | const exception = error; | |
| 47 | if ((exception.code === 'ERR_UNSUPPORTED_DIR_IMPORT' || exception.code === 'ERR_MODULE_NOT_FOUND') && typeof exception.url === 'string') { | |
| 48 | return exception.url; | |
| 49 | } | |
| 50 | throw error; | |
| 51 | } | |
| 52 | } |
src/file-viewer/highlight-grammar/toml.plist created+28| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | // transform | |
| 2 | function transform(code, optsOrCallback, maybeCallback) { | |
| 3 | let opts; | |
| 4 | let callback; | |
| 5 | if (typeof optsOrCallback === "function") { | |
| 6 | callback = optsOrCallback; | |
| 7 | opts = undefined; | |
| 8 | } else { | |
| 9 | opts = optsOrCallback; | |
| 10 | callback = maybeCallback; | |
| 11 | } | |
| 12 | if (callback === undefined) { | |
| 13 | { | |
| 14 | return (0, _rewriteStackTrace.beginHiddenCallStack)(transformRunner.sync)(code, opts); | |
| 15 | } | |
| 16 | } | |
| 17 | (0, _rewriteStackTrace.beginHiddenCallStack)(transformRunner.errback)(code, opts, callback); | |
| 18 | } | |
| 19 | ||
| 20 | // transformAsync | |
| 21 | function transformAsync(...args) { | |
| 22 | return (0, _rewriteStackTrace.beginHiddenCallStack)(transformRunner.async)(...args); | |
| 23 | } | |
| 24 | ||
| 25 | // transformSync | |
| 26 | function transformSync(...args) { | |
| 27 | return (0, _rewriteStackTrace.beginHiddenCallStack)(transformRunner.sync)(...args); | |
| 28 | } |
src/file-viewer/highlight-grammar/ts.plist created+28| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | // transformFromAst | |
| 2 | function transformFromAst(ast, code, optsOrCallback, maybeCallback) { | |
| 3 | let opts; | |
| 4 | let callback; | |
| 5 | if (typeof optsOrCallback === "function") { | |
| 6 | callback = optsOrCallback; | |
| 7 | opts = undefined; | |
| 8 | } else { | |
| 9 | opts = optsOrCallback; | |
| 10 | callback = maybeCallback; | |
| 11 | } | |
| 12 | if (callback === undefined) { | |
| 13 | { | |
| 14 | return (0, _rewriteStackTrace.beginHiddenCallStack)(transformFromAstRunner.sync)(ast, code, opts); | |
| 15 | } | |
| 16 | } | |
| 17 | (0, _rewriteStackTrace.beginHiddenCallStack)(transformFromAstRunner.errback)(ast, code, opts, callback); | |
| 18 | } | |
| 19 | ||
| 20 | // transformFromAstAsync | |
| 21 | function transformFromAstAsync(...args) { | |
| 22 | return (0, _rewriteStackTrace.beginHiddenCallStack)(transformFromAstRunner.async)(...args); | |
| 23 | } | |
| 24 | ||
| 25 | // transformFromAstSync | |
| 26 | function transformFromAstSync(...args) { | |
| 27 | return (0, _rewriteStackTrace.beginHiddenCallStack)(transformFromAstRunner.sync)(...args); | |
| 28 | } |
src/file-viewer/highlight-grammar/tsx.plist created+23| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | // parse | |
| 2 | function parse(code, opts, callback) { | |
| 3 | if (typeof opts === "function") { | |
| 4 | callback = opts; | |
| 5 | opts = undefined; | |
| 6 | } | |
| 7 | if (callback === undefined) { | |
| 8 | { | |
| 9 | return (0, _rewriteStackTrace.beginHiddenCallStack)(parseRunner.sync)(code, opts); | |
| 10 | } | |
| 11 | } | |
| 12 | (0, _rewriteStackTrace.beginHiddenCallStack)(parseRunner.errback)(code, opts, callback); | |
| 13 | } | |
| 14 | ||
| 15 | // parseAsync | |
| 16 | function parseAsync(...args) { | |
| 17 | return (0, _rewriteStackTrace.beginHiddenCallStack)(parseRunner.async)(...args); | |
| 18 | } | |
| 19 | ||
| 20 | // parseSync | |
| 21 | function parseSync(...args) { | |
| 22 | return (0, _rewriteStackTrace.beginHiddenCallStack)(parseRunner.sync)(...args); | |
| 23 | } |
src/file-viewer/highlight-grammar/xml.plist created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | // default | |
| 2 | (api, options, dirname) => { | |
| 3 | let clonedApi; | |
| 4 | for (const name of Object.keys(apiPolyfills)) { | |
| 5 | if (api[name]) continue; | |
| 6 | clonedApi != null ? clonedApi : clonedApi = copyApiObject(api); | |
| 7 | clonedApi[name] = apiPolyfills[name](clonedApi); | |
| 8 | } | |
| 9 | return builder(clonedApi != null ? clonedApi : api, options || {}, dirname); | |
| 10 | } |
src/file-viewer/highlight-grammar/yaml.plist created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | // declare | |
| 2 | function declare(builder) { | |
| 3 | return (api, options, dirname) => { | |
| 4 | let clonedApi; | |
| 5 | for (const name of Object.keys(apiPolyfills)) { | |
| 6 | if (api[name]) continue; | |
| 7 | clonedApi != null ? clonedApi : clonedApi = copyApiObject(api); | |
| 8 | clonedApi[name] = apiPolyfills[name](clonedApi); | |
| 9 | } | |
| 10 | return builder(clonedApi != null ? clonedApi : api, options || {}, dirname); | |
| 11 | }; | |
| 12 | } | |
| 13 | ||
| 14 | // declarePreset | |
| 15 | function declare(builder) { | |
| 16 | return (api, options, dirname) => { | |
| 17 | let clonedApi; | |
| 18 | for (const name of Object.keys(apiPolyfills)) { | |
| 19 | if (api[name]) continue; | |
| 20 | clonedApi != null ? clonedApi : clonedApi = copyApiObject(api); | |
| 21 | clonedApi[name] = apiPolyfills[name](clonedApi); | |
| 22 | } | |
| 23 | return builder(clonedApi != null ? clonedApi : api, options || {}, dirname); | |
| 24 | }; | |
| 25 | } |
src/file-viewer/highlight-grammar/zig.plist created+17| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | // default | |
| 2 | (api, options, dirname) => { | |
| 3 | let clonedApi; | |
| 4 | for (const name of Object.keys(apiPolyfills)) { | |
| 5 | if (api[name]) continue; | |
| 6 | clonedApi != null ? clonedApi : clonedApi = copyApiObject(api); | |
| 7 | clonedApi[name] = apiPolyfills[name](clonedApi); | |
| 8 | } | |
| 9 | return builder(clonedApi != null ? clonedApi : api, options || {}, dirname); | |
| 10 | } | |
| 11 | ||
| 12 | // defineCommonJSHook | |
| 13 | function defineCommonJSHook(file, hook) { | |
| 14 | let hooks = file.get(commonJSHooksKey); | |
| 15 | if (!hooks) file.set(commonJSHooksKey, hooks = []); | |
| 16 | hooks.push(hook); | |
| 17 | } |
src/file-viewer/highlight.ts created+200| ... | ... | @@ -0,0 +1,200 @@ |
| 1 | // buildDynamicImport | |
| 2 | function buildDynamicImport(node, deferToThen, wrapWithPromise, builder) { | |
| 3 | const specifier = _core.types.isCallExpression(node) ? node.arguments[0] : node.source; | |
| 4 | if (_core.types.isStringLiteral(specifier) || _core.types.isTemplateLiteral(specifier) && specifier.quasis.length === 0) { | |
| 5 | if (deferToThen) { | |
| 6 | return _core.template.expression.ast` | |
| 7 | Promise.resolve().then(() => ${builder(specifier)}) | |
| 8 | `; | |
| 9 | } else return builder(specifier); | |
| 10 | } | |
| 11 | const specifierToString = _core.types.isTemplateLiteral(specifier) ? _core.types.identifier("specifier") : _core.types.templateLiteral([_core.types.templateElement({ | |
| 12 | raw: "" | |
| 13 | }), _core.types.templateElement({ | |
| 14 | raw: "" | |
| 15 | })], [_core.types.identifier("specifier")]); | |
| 16 | if (deferToThen) { | |
| 17 | return _core.template.expression.ast` | |
| 18 | (specifier => | |
| 19 | new Promise(r => r(${specifierToString})) | |
| 20 | .then(s => ${builder(_core.types.identifier("s"))}) | |
| 21 | )(${specifier}) | |
| 22 | `; | |
| 23 | } else if (wrapWithPromise) { | |
| 24 | return _core.template.expression.ast` | |
| 25 | (specifier => | |
| 26 | new Promise(r => r(${builder(specifierToString)})) | |
| 27 | )(${specifier}) | |
| 28 | `; | |
| 29 | } else { | |
| 30 | return _core.template.expression.ast` | |
| 31 | (specifier => ${builder(specifierToString)})(${specifier}) | |
| 32 | `; | |
| 33 | } | |
| 34 | } | |
| 35 | ||
| 36 | // buildNamespaceInitStatements | |
| 37 | function buildNamespaceInitStatements(metadata, sourceMetadata, constantReexports = false, wrapReference = Lazy.wrapReference) { | |
| 38 | var _wrapReference; | |
| 39 | const statements = []; | |
| 40 | const srcNamespaceId = _core.types.identifier(sourceMetadata.name); | |
| 41 | for (const localName of sourceMetadata.importsNamespace) { | |
| 42 | if (localName === sourceMetadata.name) continue; | |
| 43 | statements.push(_core.template.statement`var NAME = SOURCE;`({ | |
| 44 | NAME: localName, | |
| 45 | SOURCE: _core.types.cloneNode(srcNamespaceId) | |
| 46 | })); | |
| 47 | } | |
| 48 | const srcNamespace = (_wrapReference = wrapReference(srcNamespaceId, sourceMetadata.wrap)) != null ? _wrapReference : srcNamespaceId; | |
| 49 | if (constantReexports) { | |
| 50 | statements.push(...buildReexportsFromMeta(metadata, sourceMetadata, true, wrapReference)); | |
| 51 | } | |
| 52 | for (const exportName of sourceMetadata.reexportNamespace) { | |
| 53 | statements.push((!_core.types.isIdentifier(srcNamespace) ? _core.template.statement` | |
| 54 | Object.defineProperty(EXPORTS, "NAME", { | |
| 55 | enumerable: true, | |
| 56 | get: function() { | |
| 57 | return NAMESPACE; | |
| 58 | } | |
| 59 | }); | |
| 60 | ` : _core.template.statement`EXPORTS.NAME = NAMESPACE;`)({ | |
| 61 | EXPORTS: metadata.exportName, | |
| 62 | NAME: exportName, | |
| 63 | NAMESPACE: _core.types.cloneNode(srcNamespace) | |
| 64 | })); | |
| 65 | } | |
| 66 | if (sourceMetadata.reexportAll) { | |
| 67 | const statement = buildNamespaceReexport(metadata, _core.types.cloneNode(srcNamespace), constantReexports); | |
| 68 | statement.loc = sourceMetadata.reexportAll.loc; | |
| 69 | statements.push(statement); | |
| 70 | } | |
| 71 | return statements; | |
| 72 | } | |
| 73 | ||
| 74 | // ensureStatementsHoisted | |
| 75 | function ensureStatementsHoisted(statements) { | |
| 76 | statements.forEach(header => { | |
| 77 | header._blockHoist = 3; | |
| 78 | }); | |
| 79 | } | |
| 80 | ||
| 81 | // getModuleName | |
| 82 | function getModuleName(rootOpts, pluginOpts) { | |
| 83 | var _pluginOpts$moduleId, _pluginOpts$moduleIds, _pluginOpts$getModule, _pluginOpts$moduleRoo; | |
| 84 | return originalGetModuleName(rootOpts, { | |
| 85 | moduleId: (_pluginOpts$moduleId = pluginOpts.moduleId) != null ? _pluginOpts$moduleId : rootOpts.moduleId, | |
| 86 | moduleIds: (_pluginOpts$moduleIds = pluginOpts.moduleIds) != null ? _pluginOpts$moduleIds : rootOpts.moduleIds, | |
| 87 | getModuleId: (_pluginOpts$getModule = pluginOpts.getModuleId) != null ? _pluginOpts$getModule : rootOpts.getModuleId, | |
| 88 | moduleRoot: (_pluginOpts$moduleRoo = pluginOpts.moduleRoot) != null ? _pluginOpts$moduleRoo : rootOpts.moduleRoot | |
| 89 | }); | |
| 90 | } | |
| 91 | ||
| 92 | // hasExports | |
| 93 | function hasExports(metadata) { | |
| 94 | return metadata.hasExports; | |
| 95 | } | |
| 96 | ||
| 97 | // isModule | |
| 98 | function isModule(path) { | |
| 99 | return path.node.sourceType === "module"; | |
| 100 | } | |
| 101 | ||
| 102 | // isSideEffectImport | |
| 103 | function isSideEffectImport(source) { | |
| 104 | return source.imports.size === 0 && source.importsNamespace.size === 0 && source.reexports.size === 0 && source.reexportNamespace.size === 0 && !source.reexportAll; | |
| 105 | } | |
| 106 | ||
| 107 | // rewriteModuleStatementsAndPrepareHeader | |
| 108 | function rewriteModuleStatementsAndPrepareHeader(path, { | |
| 109 | exportName, | |
| 110 | strict, | |
| 111 | allowTopLevelThis, | |
| 112 | strictMode, | |
| 113 | noInterop, | |
| 114 | importInterop = noInterop ? "none" : "babel", | |
| 115 | lazy, | |
| 116 | getWrapperPayload = Lazy.toGetWrapperPayload(lazy != null ? lazy : false), | |
| 117 | wrapReference = Lazy.wrapReference, | |
| 118 | esNamespaceOnly, | |
| 119 | filename, | |
| 120 | constantReexports = arguments[1].loose, | |
| 121 | enumerableModuleMeta = arguments[1].loose, | |
| 122 | noIncompleteNsImportDetection | |
| 123 | }) { | |
| 124 | (0, _normalizeAndLoadMetadata.validateImportInteropOption)(importInterop); | |
| 125 | _assert((0, _helperModuleImports.isModule)(path), "Cannot process module statements in a script"); | |
| 126 | path.node.sourceType = "script"; | |
| 127 | const meta = (0, _normalizeAndLoadMetadata.default)(path, exportName, { | |
| 128 | importInterop, | |
| 129 | initializeReexports: constantReexports, | |
| 130 | getWrapperPayload, | |
| 131 | esNamespaceOnly, | |
| 132 | filename | |
| 133 | }); | |
| 134 | if (!allowTopLevelThis) { | |
| 135 | (0, _rewriteThis.default)(path); | |
| 136 | } | |
| 137 | (0, _rewriteLiveReferences.default)(path, meta, wrapReference); | |
| 138 | if (strictMode !== false) { | |
| 139 | const hasStrict = path.node.directives.some(directive => { | |
| 140 | return directive.value.value === "use strict"; | |
| 141 | }); | |
| 142 | if (!hasStrict) { | |
| 143 | path.unshiftContainer("directives", _core.types.directive(_core.types.directiveLiteral("use strict"))); | |
| 144 | } | |
| 145 | } | |
| 146 | const headers = []; | |
| 147 | if ((0, _normalizeAndLoadMetadata.hasExports)(meta) && !strict) { | |
| 148 | headers.push(buildESModuleHeader(meta, enumerableModuleMeta)); | |
| 149 | } | |
| 150 | const nameList = buildExportNameListDeclaration(path, meta); | |
| 151 | if (nameList) { | |
| 152 | meta.exportNameListName = nameList.name; | |
| 153 | headers.push(nameList.statement); | |
| 154 | } | |
| 155 | headers.push(...buildExportInitializationStatements(path, meta, wrapReference, constantReexports, noIncompleteNsImportDetection)); | |
| 156 | return { | |
| 157 | meta, | |
| 158 | headers | |
| 159 | }; | |
| 160 | } | |
| 161 | ||
| 162 | // rewriteThis | |
| 163 | function rewriteThis(programPath) { | |
| 164 | if (!rewriteThisVisitor) { | |
| 165 | rewriteThisVisitor = _traverse.visitors.environmentVisitor({ | |
| 166 | ThisExpression(path) { | |
| 167 | path.replaceWith(_core.types.unaryExpression("void", _core.types.numericLiteral(0), true)); | |
| 168 | } | |
| 169 | }); | |
| 170 | rewriteThisVisitor.noScope = true; | |
| 171 | } | |
| 172 | (0, _traverse.default)(programPath.node, rewriteThisVisitor); | |
| 173 | } | |
| 174 | ||
| 175 | // wrapInterop | |
| 176 | function wrapInterop(programPath, expr, type) { | |
| 177 | if (type === "none") { | |
| 178 | return null; | |
| 179 | } | |
| 180 | if (type === "node-namespace") { | |
| 181 | return _core.types.callExpression(programPath.hub.addHelper("interopRequireWildcard"), [expr, _core.types.booleanLiteral(true)]); | |
| 182 | } else if (type === "node-default") { | |
| 183 | return null; | |
| 184 | } | |
| 185 | let helper; | |
| 186 | if (type === "default") { | |
| 187 | helper = "interopRequireDefault"; | |
| 188 | } else if (type === "namespace") { | |
| 189 | helper = "interopRequireWildcard"; | |
| 190 | } else { | |
| 191 | throw new Error(`Unknown interop: ${type}`); | |
| 192 | } | |
| 193 | return _core.types.callExpression(programPath.hub.addHelper(helper), [expr]); | |
| 194 | } | |
| 195 | ||
| 196 | // getDynamicImportSource | |
| 197 | function getDynamicImportSource(node) { | |
| 198 | const [source] = node.arguments; | |
| 199 | return _core.types.isStringLiteral(source) || _core.types.isTemplateLiteral(source) ? source : _core.template.expression.ast`\`\${${source}}\``; | |
| 200 | } |
src/file-viewer/mime.json created+463| ... | ... | @@ -0,0 +1,463 @@ |
| 1 | ||
| 2 | ".aac": "audio/x-aac", | |
| 3 | ".aif": "audio/x-aiff", | |
| 4 | ".aifc": "audio/x-aiff", | |
| 5 | ".aiff": "audio/x-aiff", | |
| 6 | ".apk": "application/vnd.android.package-archive", | |
| 7 | ".asm": "text/x-asm", | |
| 8 | ".bat": "application/x-msdownload", | |
| 9 | ".bmp": "image/bmp", | |
| 10 | ".c": "text/x-c", | |
| 11 | ".cc": "text/x-c", | |
| 12 | ".class": "application/java-vm", | |
| 13 | ".com": "application/x-msdownload", | |
| 14 | ".conf": "text/plain", | |
| 15 | ".cpp": "text/x-c", | |
| 16 | ".css": "text/css", | |
| 17 | ".cxx": "text/x-c", | |
| 18 | ".def": "text/plain", | |
| 19 | ".diff": "text/plain", | |
| 20 | ".dll": "application/x-msdownload", | |
| 21 | ".doc": "application/msword", | |
| 22 | ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| 23 | ".epub": "application/epub+zip", | |
| 24 | ".exe": "application/x-msdownload", | |
| 25 | ".gif": "image/gif", | |
| 26 | ".gz": "application/x-gzip", | |
| 27 | ".h": "text/x-c", | |
| 28 | ".hh": "text/x-c", | |
| 29 | ".htm": "text/html;charset=utf-8", | |
| 30 | ".html": "text/html;charset=utf-8", | |
| 31 | ".ico": "image/x-icon", | |
| 32 | ".ics": "text/calendar", | |
| 33 | ".ifb": "text/calendar", | |
| 34 | ".iso": "application/octet-stream", | |
| 35 | ".jar": "application/java-archive", | |
| 36 | ".java": "text/x-java-source", | |
| 37 | ".jpe": "image/jpeg", | |
| 38 | ".jpeg": "image/jpeg", | |
| 39 | ".jpg": "image/jpeg", | |
| 40 | ".jpgv": "video/jpeg", | |
| 41 | ".js": "application/javascript", | |
| 42 | ".json": "application/json", | |
| 43 | ".latex": "application/x-latex", | |
| 44 | ".list": "text/plain", | |
| 45 | ".log": "text/plain", | |
| 46 | ".chat": "text/plain", | |
| 47 | ".m4a": "audio/mp4", | |
| 48 | ".mid": "audio/midi", | |
| 49 | ".midi": "audio/midi", | |
| 50 | ".mov": "video/quicktime", | |
| 51 | ".mp3": "audio/mpeg", | |
| 52 | ".mp4": "video/mp4", | |
| 53 | ".mp4a": "audio/mp4", | |
| 54 | ".mp4v": "video/mp4", | |
| 55 | ".mpa": "video/mpeg", | |
| 56 | ".mpe": "video/mpeg", | |
| 57 | ".mpeg": "video/mpeg", | |
| 58 | ".mpg": "video/mpeg", | |
| 59 | ".mpg4": "video/mp4", | |
| 60 | ".mpga": "audio/mpeg", | |
| 61 | ".mpkg": "application/vnd.apple.installer+xml", | |
| 62 | ".msi": "application/x-msdownload", | |
| 63 | ||
| 64 | ".nb": "application/mathematica", | |
| 65 | ".nc": "application/x-netcdf", | |
| 66 | ".ncx": "application/x-dtbncx+xml", | |
| 67 | ".ngdat": "application/vnd.nokia.n-gage.data", | |
| 68 | ".nlu": "application/vnd.neurolanguage.nlu", | |
| 69 | ".nml": "application/vnd.enliven", | |
| 70 | ".nnd": "application/vnd.noblenet-directory", | |
| 71 | ".nns": "application/vnd.noblenet-sealer", | |
| 72 | ".nnw": "application/vnd.noblenet-web", | |
| 73 | ".npx": "image/vnd.net-fpx", | |
| 74 | ".nsf": "application/vnd.lotus-notes", | |
| 75 | ".nws": "message/rfc822", | |
| 76 | ".o": "application/octet-stream", | |
| 77 | ".oa2": "application/vnd.fujitsu.oasys2", | |
| 78 | ".oa3": "application/vnd.fujitsu.oasys3", | |
| 79 | ".oas": "application/vnd.fujitsu.oasys", | |
| 80 | ".obd": "application/x-msbinder", | |
| 81 | ".obj": "application/octet-stream", | |
| 82 | ".oda": "application/oda", | |
| 83 | ".odb": "application/vnd.oasis.opendocument.database", | |
| 84 | ".odc": "application/vnd.oasis.opendocument.chart", | |
| 85 | ".odf": "application/vnd.oasis.opendocument.formula", | |
| 86 | ".odft": "application/vnd.oasis.opendocument.formula-template", | |
| 87 | ".odg": "application/vnd.oasis.opendocument.graphics", | |
| 88 | ".odi": "application/vnd.oasis.opendocument.image", | |
| 89 | ".odp": "application/vnd.oasis.opendocument.presentation", | |
| 90 | ".ods": "application/vnd.oasis.opendocument.spreadsheet", | |
| 91 | ".odt": "application/vnd.oasis.opendocument.text", | |
| 92 | ".oga": "audio/ogg", | |
| 93 | ".ogg": "audio/ogg", | |
| 94 | ".ogv": "video/ogg", | |
| 95 | ".ogx": "application/ogg", | |
| 96 | ".onepkg": "application/onenote", | |
| 97 | ".onetmp": "application/onenote", | |
| 98 | ".onetoc": "application/onenote", | |
| 99 | ".onetoc2": "application/onenote", | |
| 100 | ".opf": "application/oebps-package+xml", | |
| 101 | ".oprc": "application/vnd.palm", | |
| 102 | ".org": "application/vnd.lotus-organizer", | |
| 103 | ".osf": "application/vnd.yamaha.openscoreformat", | |
| 104 | ".osfpvg": "application/vnd.yamaha.openscoreformat.osfpvg+xml", | |
| 105 | ".otc": "application/vnd.oasis.opendocument.chart-template", | |
| 106 | ".otf": "application/x-font-otf", | |
| 107 | ".otg": "application/vnd.oasis.opendocument.graphics-template", | |
| 108 | ".oth": "application/vnd.oasis.opendocument.text-web", | |
| 109 | ".oti": "application/vnd.oasis.opendocument.image-template", | |
| 110 | ".otm": "application/vnd.oasis.opendocument.text-master", | |
| 111 | ".otp": "application/vnd.oasis.opendocument.presentation-template", | |
| 112 | ".ots": "application/vnd.oasis.opendocument.spreadsheet-template", | |
| 113 | ".ott": "application/vnd.oasis.opendocument.text-template", | |
| 114 | ".oxt": "application/vnd.openofficeorg.extension", | |
| 115 | ".p": "text/x-pascal", | |
| 116 | ".p10": "application/pkcs10", | |
| 117 | ".p12": "application/x-pkcs12", | |
| 118 | ".p7b": "application/x-pkcs7-certificates", | |
| 119 | ".p7c": "application/pkcs7-mime", | |
| 120 | ".p7m": "application/pkcs7-mime", | |
| 121 | ".p7r": "application/x-pkcs7-certreqresp", | |
| 122 | ".p7s": "application/pkcs7-signature", | |
| 123 | ".pas": "text/x-pascal", | |
| 124 | ".pbd": "application/vnd.powerbuilder6", | |
| 125 | ".pbm": "image/x-portable-bitmap", | |
| 126 | ".pcf": "application/x-font-pcf", | |
| 127 | ".pcl": "application/vnd.hp-pcl", | |
| 128 | ".pclxl": "application/vnd.hp-pclxl", | |
| 129 | ".pct": "image/x-pict", | |
| 130 | ".pcurl": "application/vnd.curl.pcurl", | |
| 131 | ".pcx": "image/x-pcx", | |
| 132 | ".pdb": "application/vnd.palm", | |
| 133 | ".pdf": "application/pdf", | |
| 134 | ".pfa": "application/x-font-type1", | |
| 135 | ".pfb": "application/x-font-type1", | |
| 136 | ".pfm": "application/x-font-type1", | |
| 137 | ".pfr": "application/font-tdpfr", | |
| 138 | ".pfx": "application/x-pkcs12", | |
| 139 | ".pgm": "image/x-portable-graymap", | |
| 140 | ".pgn": "application/x-chess-pgn", | |
| 141 | ".pgp": "application/pgp-encrypted", | |
| 142 | ".pic": "image/x-pict", | |
| 143 | ".pkg": "application/octet-stream", | |
| 144 | ".pki": "application/pkixcmp", | |
| 145 | ".pkipath": "application/pkix-pkipath", | |
| 146 | ".pl": "text/plain", | |
| 147 | ".plb": "application/vnd.3gpp.pic-bw-large", | |
| 148 | ".plc": "application/vnd.mobius.plc", | |
| 149 | ".plf": "application/vnd.pocketlearn", | |
| 150 | ".pls": "application/pls+xml", | |
| 151 | ".pml": "application/vnd.ctc-posml", | |
| 152 | ".png": "image/png", | |
| 153 | ".pnm": "image/x-portable-anymap", | |
| 154 | ".portpkg": "application/vnd.macports.portpkg", | |
| 155 | ".pot": "application/vnd.ms-powerpoint", | |
| 156 | ".potm": "application/vnd.ms-powerpoint.template.macroenabled.12", | |
| 157 | ".potx": "application/vnd.openxmlformats-officedocument.presentationml.template", | |
| 158 | ".ppa": "application/vnd.ms-powerpoint", | |
| 159 | ".ppam": "application/vnd.ms-powerpoint.addin.macroenabled.12", | |
| 160 | ".ppd": "application/vnd.cups-ppd", | |
| 161 | ".ppm": "image/x-portable-pixmap", | |
| 162 | ".pps": "application/vnd.ms-powerpoint", | |
| 163 | ".ppsm": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", | |
| 164 | ".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", | |
| 165 | ".ppt": "application/vnd.ms-powerpoint", | |
| 166 | ".pptm": "application/vnd.ms-powerpoint.presentation.macroenabled.12", | |
| 167 | ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", | |
| 168 | ".pqa": "application/vnd.palm", | |
| 169 | ".prc": "application/x-mobipocket-ebook", | |
| 170 | ".pre": "application/vnd.lotus-freelance", | |
| 171 | ".prf": "application/pics-rules", | |
| 172 | ".ps": "application/postscript", | |
| 173 | ".psb": "application/vnd.3gpp.pic-bw-small", | |
| 174 | ".psd": "image/vnd.adobe.photoshop", | |
| 175 | ".psf": "application/x-font-linux-psf", | |
| 176 | ".ptid": "application/vnd.pvi.ptid1", | |
| 177 | ".pub": "application/x-mspublisher", | |
| 178 | ".pvb": "application/vnd.3gpp.pic-bw-var", | |
| 179 | ".pwn": "application/vnd.3m.post-it-notes", | |
| 180 | ".pwz": "application/vnd.ms-powerpoint", | |
| 181 | ".py": "text/x-python", | |
| 182 | ".pya": "audio/vnd.ms-playready.media.pya", | |
| 183 | ".pyc": "application/x-python-code", | |
| 184 | ".pyo": "application/x-python-code", | |
| 185 | ".pyv": "video/vnd.ms-playready.media.pyv", | |
| 186 | ".qam": "application/vnd.epson.quickanime", | |
| 187 | ".qbo": "application/vnd.intu.qbo", | |
| 188 | ".qfx": "application/vnd.intu.qfx", | |
| 189 | ".qps": "application/vnd.publishare-delta-tree", | |
| 190 | ".qt": "video/quicktime", | |
| 191 | ".qwd": "application/vnd.quark.quarkxpress", | |
| 192 | ".qwt": "application/vnd.quark.quarkxpress", | |
| 193 | ".qxb": "application/vnd.quark.quarkxpress", | |
| 194 | ".qxd": "application/vnd.quark.quarkxpress", | |
| 195 | ".qxl": "application/vnd.quark.quarkxpress", | |
| 196 | ".qxt": "application/vnd.quark.quarkxpress", | |
| 197 | ".ra": "audio/x-pn-realaudio", | |
| 198 | ".ram": "audio/x-pn-realaudio", | |
| 199 | ".rar": "application/x-rar-compressed", | |
| 200 | ".ras": "image/x-cmu-raster", | |
| 201 | ".rcprofile": "application/vnd.ipunplugged.rcprofile", | |
| 202 | ".rdf": "application/rdf+xml", | |
| 203 | ".rdz": "application/vnd.data-vision.rdz", | |
| 204 | ".rep": "application/vnd.businessobjects", | |
| 205 | ".res": "application/x-dtbresource+xml", | |
| 206 | ".rgb": "image/x-rgb", | |
| 207 | ".rif": "application/reginfo+xml", | |
| 208 | ".rl": "application/resource-lists+xml", | |
| 209 | ".rlc": "image/vnd.fujixerox.edmics-rlc", | |
| 210 | ".rld": "application/resource-lists-diff+xml", | |
| 211 | ".rm": "application/vnd.rn-realmedia", | |
| 212 | ".rmi": "audio/midi", | |
| 213 | ".rmp": "audio/x-pn-realaudio-plugin", | |
| 214 | ".rms": "application/vnd.jcp.javame.midlet-rms", | |
| 215 | ".rnc": "application/relax-ng-compact-syntax", | |
| 216 | ".roff": "text/troff", | |
| 217 | ".rpm": "application/x-rpm", | |
| 218 | ".rpss": "application/vnd.nokia.radio-presets", | |
| 219 | ".rpst": "application/vnd.nokia.radio-preset", | |
| 220 | ".rq": "application/sparql-query", | |
| 221 | ".rs": "application/rls-services+xml", | |
| 222 | ".rsd": "application/rsd+xml", | |
| 223 | ".rss": "application/rss+xml", | |
| 224 | ".rtf": "application/rtf", | |
| 225 | ".rtx": "text/richtext", | |
| 226 | ".s": "text/x-asm", | |
| 227 | ".saf": "application/vnd.yamaha.smaf-audio", | |
| 228 | ".sbml": "application/sbml+xml", | |
| 229 | ".sc": "application/vnd.ibm.secure-container", | |
| 230 | ".scd": "application/x-msschedule", | |
| 231 | ".scm": "application/vnd.lotus-screencam", | |
| 232 | ".scq": "application/scvp-cv-request", | |
| 233 | ".scs": "application/scvp-cv-response", | |
| 234 | ".scurl": "text/vnd.curl.scurl", | |
| 235 | ".sda": "application/vnd.stardivision.draw", | |
| 236 | ".sdc": "application/vnd.stardivision.calc", | |
| 237 | ".sdd": "application/vnd.stardivision.impress", | |
| 238 | ".sdkd": "application/vnd.solent.sdkm+xml", | |
| 239 | ".sdkm": "application/vnd.solent.sdkm+xml", | |
| 240 | ".sdp": "application/sdp", | |
| 241 | ".sdw": "application/vnd.stardivision.writer", | |
| 242 | ".see": "application/vnd.seemail", | |
| 243 | ".seed": "application/vnd.fdsn.seed", | |
| 244 | ".sema": "application/vnd.sema", | |
| 245 | ".semd": "application/vnd.semd", | |
| 246 | ".semf": "application/vnd.semf", | |
| 247 | ".ser": "application/java-serialized-object", | |
| 248 | ".setpay": "application/set-payment-initiation", | |
| 249 | ".setreg": "application/set-registration-initiation", | |
| 250 | ".sfd-hdstx": "application/vnd.hydrostatix.sof-data", | |
| 251 | ".sfs": "application/vnd.spotfire.sfs", | |
| 252 | ".sgl": "application/vnd.stardivision.writer-global", | |
| 253 | ".sgm": "text/sgml", | |
| 254 | ".sgml": "text/sgml", | |
| 255 | ".sh": "application/x-sh", | |
| 256 | ".shar": "application/x-shar", | |
| 257 | ".shf": "application/shf+xml", | |
| 258 | ".si": "text/vnd.wap.si", | |
| 259 | ".sic": "application/vnd.wap.sic", | |
| 260 | ".sig": "application/pgp-signature", | |
| 261 | ".silo": "model/mesh", | |
| 262 | ".sis": "application/vnd.symbian.install", | |
| 263 | ".sisx": "application/vnd.symbian.install", | |
| 264 | ".sit": "application/x-stuffit", | |
| 265 | ".sitx": "application/x-stuffitx", | |
| 266 | ".skd": "application/vnd.koan", | |
| 267 | ".skm": "application/vnd.koan", | |
| 268 | ".skp": "application/vnd.koan", | |
| 269 | ".skt": "application/vnd.koan", | |
| 270 | ".sl": "text/vnd.wap.sl", | |
| 271 | ".slc": "application/vnd.wap.slc", | |
| 272 | ".sldm": "application/vnd.ms-powerpoint.slide.macroenabled.12", | |
| 273 | ".sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide", | |
| 274 | ".slt": "application/vnd.epson.salt", | |
| 275 | ".smf": "application/vnd.stardivision.math", | |
| 276 | ".smi": "application/smil+xml", | |
| 277 | ".smil": "application/smil+xml", | |
| 278 | ".snd": "audio/basic", | |
| 279 | ".snf": "application/x-font-snf", | |
| 280 | ".so": "application/octet-stream", | |
| 281 | ".spc": "application/x-pkcs7-certificates", | |
| 282 | ".spf": "application/vnd.yamaha.smaf-phrase", | |
| 283 | ".spl": "application/x-futuresplash", | |
| 284 | ".spot": "text/vnd.in3d.spot", | |
| 285 | ".spp": "application/scvp-vp-response", | |
| 286 | ".spq": "application/scvp-vp-request", | |
| 287 | ".spx": "audio/ogg", | |
| 288 | ".src": "application/x-wais-source", | |
| 289 | ".srx": "application/sparql-results+xml", | |
| 290 | ".sse": "application/vnd.kodak-descriptor", | |
| 291 | ".ssf": "application/vnd.epson.ssf", | |
| 292 | ".ssml": "application/ssml+xml", | |
| 293 | ".stc": "application/vnd.sun.xml.calc.template", | |
| 294 | ".std": "application/vnd.sun.xml.draw.template", | |
| 295 | ".stf": "application/vnd.wt.stf", | |
| 296 | ".sti": "application/vnd.sun.xml.impress.template", | |
| 297 | ".stk": "application/hyperstudio", | |
| 298 | ".stl": "application/vnd.ms-pki.stl", | |
| 299 | ".str": "application/vnd.pg.format", | |
| 300 | ".stw": "application/vnd.sun.xml.writer.template", | |
| 301 | ".sus": "application/vnd.sus-calendar", | |
| 302 | ".susp": "application/vnd.sus-calendar", | |
| 303 | ".sv4cpio": "application/x-sv4cpio", | |
| 304 | ".sv4crc": "application/x-sv4crc", | |
| 305 | ".svd": "application/vnd.svd", | |
| 306 | ".svg": "image/svg+xml", | |
| 307 | ".svgz": "image/svg+xml", | |
| 308 | ".swa": "application/x-director", | |
| 309 | ".swf": "application/x-shockwave-flash", | |
| 310 | ".swi": "application/vnd.arastra.swi", | |
| 311 | ".sxc": "application/vnd.sun.xml.calc", | |
| 312 | ".sxd": "application/vnd.sun.xml.draw", | |
| 313 | ".sxg": "application/vnd.sun.xml.writer.global", | |
| 314 | ".sxi": "application/vnd.sun.xml.impress", | |
| 315 | ".sxm": "application/vnd.sun.xml.math", | |
| 316 | ".sxw": "application/vnd.sun.xml.writer", | |
| 317 | ".t": "text/troff", | |
| 318 | ".tao": "application/vnd.tao.intent-module-archive", | |
| 319 | ".tar": "application/x-tar", | |
| 320 | ".tcap": "application/vnd.3gpp2.tcap", | |
| 321 | ".tcl": "application/x-tcl", | |
| 322 | ".teacher": "application/vnd.smart.teacher", | |
| 323 | ".tex": "application/x-tex", | |
| 324 | ".texi": "application/x-texinfo", | |
| 325 | ".texinfo": "application/x-texinfo", | |
| 326 | ".text": "text/plain", | |
| 327 | ".tfm": "application/x-tex-tfm", | |
| 328 | ".tgz": "application/x-gzip", | |
| 329 | ".tif": "image/tiff", | |
| 330 | ".tiff": "image/tiff", | |
| 331 | ".tmo": "application/vnd.tmobile-livetv", | |
| 332 | ".torrent": "application/x-bittorrent", | |
| 333 | ".tpl": "application/vnd.groove-tool-template", | |
| 334 | ".tpt": "application/vnd.trid.tpt", | |
| 335 | ".tr": "text/troff", | |
| 336 | ".tra": "application/vnd.trueapp", | |
| 337 | ".trm": "application/x-msterminal", | |
| 338 | ".tsv": "text/tab-separated-values", | |
| 339 | ".ttc": "application/x-font-ttf", | |
| 340 | ".ttf": "application/x-font-ttf", | |
| 341 | ".twd": "application/vnd.simtech-mindmapper", | |
| 342 | ".twds": "application/vnd.simtech-mindmapper", | |
| 343 | ".txd": "application/vnd.genomatix.tuxedo", | |
| 344 | ".txf": "application/vnd.mobius.txf", | |
| 345 | ".txt": "text/plain", | |
| 346 | ".u32": "application/x-authorware-bin", | |
| 347 | ".udeb": "application/x-debian-package", | |
| 348 | ".ufd": "application/vnd.ufdl", | |
| 349 | ".ufdl": "application/vnd.ufdl", | |
| 350 | ".umj": "application/vnd.umajin", | |
| 351 | ".unityweb": "application/vnd.unity", | |
| 352 | ".uoml": "application/vnd.uoml+xml", | |
| 353 | ".uri": "text/uri-list", | |
| 354 | ".uris": "text/uri-list", | |
| 355 | ".urls": "text/uri-list", | |
| 356 | ".ustar": "application/x-ustar", | |
| 357 | ".utz": "application/vnd.uiq.theme", | |
| 358 | ".uu": "text/x-uuencode", | |
| 359 | ".vcd": "application/x-cdlink", | |
| 360 | ".vcf": "text/x-vcard", | |
| 361 | ".vcg": "application/vnd.groove-vcard", | |
| 362 | ".vcs": "text/x-vcalendar", | |
| 363 | ".vcx": "application/vnd.vcx", | |
| 364 | ".vis": "application/vnd.visionary", | |
| 365 | ".viv": "video/vnd.vivo", | |
| 366 | ".vor": "application/vnd.stardivision.writer", | |
| 367 | ".vox": "application/x-authorware-bin", | |
| 368 | ".vrml": "model/vrml", | |
| 369 | ".vsd": "application/vnd.visio", | |
| 370 | ".vsf": "application/vnd.vsf", | |
| 371 | ".vss": "application/vnd.visio", | |
| 372 | ".vst": "application/vnd.visio", | |
| 373 | ".vsw": "application/vnd.visio", | |
| 374 | ".vtu": "model/vnd.vtu", | |
| 375 | ".vxml": "application/voicexml+xml", | |
| 376 | ".w3d": "application/x-director", | |
| 377 | ".wad": "application/x-doom", | |
| 378 | ".wav": "audio/x-wav", | |
| 379 | ".wax": "audio/x-ms-wax", | |
| 380 | ".wbmp": "image/vnd.wap.wbmp", | |
| 381 | ".wbs": "application/vnd.criticaltools.wbs+xml", | |
| 382 | ".wbxml": "application/vnd.wap.wbxml", | |
| 383 | ".wcm": "application/vnd.ms-works", | |
| 384 | ".wdb": "application/vnd.ms-works", | |
| 385 | ".wiz": "application/msword", | |
| 386 | ".wks": "application/vnd.ms-works", | |
| 387 | ".wm": "video/x-ms-wm", | |
| 388 | ".wma": "audio/x-ms-wma", | |
| 389 | ".wmd": "application/x-ms-wmd", | |
| 390 | ".wmf": "application/x-msmetafile", | |
| 391 | ".wml": "text/vnd.wap.wml", | |
| 392 | ".wmlc": "application/vnd.wap.wmlc", | |
| 393 | ".wmls": "text/vnd.wap.wmlscript", | |
| 394 | ".wmlsc": "application/vnd.wap.wmlscriptc", | |
| 395 | ".wmv": "video/x-ms-wmv", | |
| 396 | ".wmx": "video/x-ms-wmx", | |
| 397 | ".wmz": "application/x-ms-wmz", | |
| 398 | ".wpd": "application/vnd.wordperfect", | |
| 399 | ".wpl": "application/vnd.ms-wpl", | |
| 400 | ".wps": "application/vnd.ms-works", | |
| 401 | ".wqd": "application/vnd.wqd", | |
| 402 | ".wri": "application/x-mswrite", | |
| 403 | ".wrl": "model/vrml", | |
| 404 | ".wsdl": "application/wsdl+xml", | |
| 405 | ".wspolicy": "application/wspolicy+xml", | |
| 406 | ".wtb": "application/vnd.webturbo", | |
| 407 | ".wvx": "video/x-ms-wvx", | |
| 408 | ".x32": "application/x-authorware-bin", | |
| 409 | ".x3d": "application/vnd.hzn-3d-crossword", | |
| 410 | ".xap": "application/x-silverlight-app", | |
| 411 | ".xar": "application/vnd.xara", | |
| 412 | ".xbap": "application/x-ms-xbap", | |
| 413 | ".xbd": "application/vnd.fujixerox.docuworks.binder", | |
| 414 | ".xbm": "image/x-xbitmap", | |
| 415 | ".xdm": "application/vnd.syncml.dm+xml", | |
| 416 | ".xdp": "application/vnd.adobe.xdp+xml", | |
| 417 | ".xdw": "application/vnd.fujixerox.docuworks", | |
| 418 | ".xenc": "application/xenc+xml", | |
| 419 | ".xer": "application/patch-ops-error+xml", | |
| 420 | ".xfdf": "application/vnd.adobe.xfdf", | |
| 421 | ".xfdl": "application/vnd.xfdl", | |
| 422 | ".xht": "application/xhtml+xml", | |
| 423 | ".xhtml": "application/xhtml+xml", | |
| 424 | ".xhvml": "application/xv+xml", | |
| 425 | ".xif": "image/vnd.xiff", | |
| 426 | ".xla": "application/vnd.ms-excel", | |
| 427 | ".xlam": "application/vnd.ms-excel.addin.macroenabled.12", | |
| 428 | ".xlb": "application/vnd.ms-excel", | |
| 429 | ".xlc": "application/vnd.ms-excel", | |
| 430 | ".xlm": "application/vnd.ms-excel", | |
| 431 | ".xls": "application/vnd.ms-excel", | |
| 432 | ".xlsb": "application/vnd.ms-excel.sheet.binary.macroenabled.12", | |
| 433 | ".xlsm": "application/vnd.ms-excel.sheet.macroenabled.12", | |
| 434 | ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |
| 435 | ".xlt": "application/vnd.ms-excel", | |
| 436 | ".xltm": "application/vnd.ms-excel.template.macroenabled.12", | |
| 437 | ".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", | |
| 438 | ".xlw": "application/vnd.ms-excel", | |
| 439 | ".xml": "application/xml", | |
| 440 | ".xo": "application/vnd.olpc-sugar", | |
| 441 | ".xop": "application/xop+xml", | |
| 442 | ".xpdl": "application/xml", | |
| 443 | ".xpi": "application/x-xpinstall", | |
| 444 | ".xpm": "image/x-xpixmap", | |
| 445 | ".xpr": "application/vnd.is-xpr", | |
| 446 | ".xps": "application/vnd.ms-xpsdocument", | |
| 447 | ".xpw": "application/vnd.intercon.formnet", | |
| 448 | ".xpx": "application/vnd.intercon.formnet", | |
| 449 | ".xsl": "application/xml", | |
| 450 | ".xslt": "application/xslt+xml", | |
| 451 | ".xsm": "application/vnd.syncml+xml", | |
| 452 | ".xspf": "application/xspf+xml", | |
| 453 | ".xul": "application/vnd.mozilla.xul+xml", | |
| 454 | ".xvm": "application/xv+xml", | |
| 455 | ".xvml": "application/xv+xml", | |
| 456 | ".xwd": "image/x-xwindowdump", | |
| 457 | ".xyz": "chemical/x-xyz", | |
| 458 | ".zaz": "application/vnd.zzazz.deck+xml", | |
| 459 | ".zip": "application/zip", | |
| 460 | ".zir": "application/vnd.zul", | |
| 461 | ".zirz": "application/vnd.zul", | |
| 462 | ".zmm": "application/vnd.handheld-entertainment+xml" | |
| 463 | } |
src/file-viewer/models/BlobAsset.ts created+55| ... | ... | @@ -0,0 +1,55 @@ |
| 1 | const db = getDb("cache.sqlite"); | |
| 2 | db.table( | |
| 3 | "blob_assets", | |
| 4 | /* SQL */ ` | |
| 5 | create table blob_assets ( | |
| 6 | hash text primary key, | |
| 7 | refs integer not null default 0 | |
| 8 | ); | |
| 9 | `, | |
| 10 | ); | |
| 11 | /** | |
| 12 | * Uncompressed files are read directly from the media store root. Compressed | |
| 13 | * files are stored as `<compress store>/<first 2 chars of hash>/<hash>` Since | |
| 14 | * multiple files can share the same hash, the number of references is tracked | |
| 15 | * so that when a file is deleted, the compressed data is only removed when all | |
| 16 | * references are gone. | |
| 17 | */ | |
| 18 | export class BlobAsset { | |
| 19 | /** sha1 of the contents */ | |
| 20 | hash!: string; | |
| 21 | refs!: number; | |
| 22 | } | |
| 23 | ||
| 24 | export namespace BlobAsset { | |
| 25 | const getQuery = cache.prepare(/* SQL */ ` | |
| 26 | SELECT * FROM blob_assets WHERE hash = ?; | |
| 27 | `).as(BlobAsset); | |
| 28 | export function get(hash: string) { | |
| 29 | return getQuery.get(hash); | |
| 30 | } | |
| 31 | ||
| 32 | const putOrIncrementQuery = cache.prepare(/* SQL */ ` | |
| 33 | INSERT INTO blob_assets (hash, refs) VALUES (?, 1) | |
| 34 | ON CONFLICT(hash) DO UPDATE SET refs = refs + 1; | |
| 35 | `); | |
| 36 | export function putOrIncrement(hash: string) { | |
| 37 | assert(hash.length === 40); | |
| 38 | putOrIncrementQuery.get(hash); | |
| 39 | return get(hash)!; | |
| 40 | } | |
| 41 | ||
| 42 | const decrementQuery = cache.prepare(/* SQL */ ` | |
| 43 | UPDATE blob_assets SET refs = refs - 1 WHERE hash = ? AND refs > 0; | |
| 44 | `); | |
| 45 | const deleteQuery = cache.prepare(/* SQL */ ` | |
| 46 | DELETE FROM blob_assets WHERE hash = ? AND refs <= 0; | |
| 47 | `); | |
| 48 | export function decrementOrDelete(hash: string) { | |
| 49 | assert(hash.length === 40); | |
| 50 | decrementQuery.run(hash); | |
| 51 | return deleteQuery.run(hash).changes > 0; | |
| 52 | } | |
| 53 | } | |
| 54 | ||
| 55 | import { getDb } from "#sqlite"; |
src/file-viewer/models/FilePermissions.ts createdsrc/file-viewer/models/MediaFile.ts created+51| ... | ... | @@ -0,0 +1,51 @@ |
| 1 | import { getDb } from "#database"; | |
| 2 | ||
| 3 | const db = getDb("cache.sqlite"); | |
| 4 | ||
| 5 | db.table(/* SQL */ ` | |
| 6 | CREATE TABLE IF NOT EXISTS permissions ( | |
| 7 | prefix TEXT PRIMARY KEY, | |
| 8 | allow INTEGER NOT NULL | |
| 9 | ); | |
| 10 | `); | |
| 11 | ||
| 12 | export class FilePermissions { | |
| 13 | prefix!: string; | |
| 14 | /** Currently set to 1 always */ | |
| 15 | allow!: number; | |
| 16 | ||
| 17 | // -- static ops -- | |
| 18 | ||
| 19 | static getByPrefix(filePath: string): number { | |
| 20 | return (getByPrefixQuery.get(filePath))?.allow ?? 0; | |
| 21 | } | |
| 22 | static getExact(filePath: string): number { | |
| 23 | return (getExactQuery.get(filePath))?.allow ?? 0; | |
| 24 | } | |
| 25 | static setPermissions(dirPath: string, level: number) { | |
| 26 | if (level) { | |
| 27 | insertQuery.run(dirPath, level); | |
| 28 | } else { | |
| 29 | deleteQuery.run(dirPath); | |
| 30 | } | |
| 31 | } | |
| 32 | } | |
| 33 | ||
| 34 | const getByPrefixQuery = db.prepare(/* SQL */ ` | |
| 35 | SELECT allow | |
| 36 | FROM permissions | |
| 37 | WHERE ? GLOB prefix || '*' | |
| 38 | ORDER BY LENGTH(prefix) DESC | |
| 39 | LIMIT 1; | |
| 40 | `).type<FilePermissions>(); | |
| 41 | ||
| 42 | const getExactQuery = db.prepare(/* SQL */ ` | |
| 43 | SELECT allow FROM permissions WHERE ? == prefix | |
| 44 | `).type<FilePermissions>(); | |
| 45 | ||
| 46 | const insertQuery = db.prepare(/* SQL */ ` | |
| 47 | REPLACE INTO permissions(prefix, allow) VALUES(?, ?); | |
| 48 | `); | |
| 49 | const deleteQuery = db.prepare(/* SQL */ ` | |
| 50 | DELETE FROM permissions WHERE prefix = ?; | |
| 51 | `); |
src/file-viewer/pages.off/file.cotyledon_enterance.tsx created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | import { MediaFile } from "../db"; | |
| 2 | import { useInlineScript } from "../framework/page-resources"; | |
| 3 | import { Readme } from "../media/cotyledon"; | |
| 4 | import { MediaPanel } from "../pages-dynamic/file_viewer"; | |
| 5 | import "../media/files.css"; | |
| 6 | ||
| 7 | export const theme = { | |
| 8 | bg: "#312652", | |
| 9 | fg: "#f0f0ff", | |
| 10 | primary: "#fabe32", | |
| 11 | }; | |
| 12 | ||
| 13 | export default function CotyledonPage() { | |
| 14 | useInlineScript("canvas_cotyledon"); | |
| 15 | useInlineScript("file_viewer"); | |
| 16 | return ( | |
| 17 | <div class="files ctld ctld-et"> | |
| 18 | <MediaPanel | |
| 19 | file={MediaFile.getByPath("/")!} | |
| 20 | isLast={false} | |
| 21 | activeFilename={null} | |
| 22 | hasCotyledonCookie | |
| 23 | /> | |
| 24 | <Readme /> | |
| 25 | </div> | |
| 26 | ); | |
| 27 | } |
src/file-viewer/pages.off/file.cotyledon_speedbump.tsx created+43| ... | ... | @@ -0,0 +1,43 @@ |
| 1 | // relativeImportPath | |
| 2 | function relativeImportPath(from, to) { | |
| 3 | let i = 0; | |
| 4 | let sepPos = -1; | |
| 5 | let prevSepPos = -1; | |
| 6 | let prevPrevSepPos = -1; | |
| 7 | const fromLen = from.length; | |
| 8 | const commonLen = Math.min(to.length, fromLen); | |
| 9 | for (; i < commonLen; i++) { | |
| 10 | const curChar = to[i]; | |
| 11 | if (curChar !== from[i]) | |
| 12 | break; | |
| 13 | if (curChar === import_path.sep) { | |
| 14 | prevPrevSepPos = prevSepPos; | |
| 15 | prevSepPos = sepPos; | |
| 16 | sepPos = i; | |
| 17 | } | |
| 18 | } | |
| 19 | if (sepPos !== -1) { | |
| 20 | if (hasNms(to, sepPos)) { | |
| 21 | return toPosix(stripNms(to, sepPos)); | |
| 22 | } | |
| 23 | if (prevSepPos !== -1) { | |
| 24 | if (prevPrevSepPos !== -1 && to[prevSepPos + 1] === "@") { | |
| 25 | prevSepPos = prevPrevSepPos; | |
| 26 | } | |
| 27 | if (hasNms(to, prevSepPos)) { | |
| 28 | return toPosix(stripNms(to, prevSepPos)); | |
| 29 | } | |
| 30 | } | |
| 31 | } | |
| 32 | if (sepPos <= 0) | |
| 33 | return toPosix(to); | |
| 34 | let back = 0; | |
| 35 | for (; i < fromLen; i++) | |
| 36 | if (from[i] === import_path.sep) | |
| 37 | back++; | |
| 38 | if (back) { | |
| 39 | return backSep.repeat(back) + toPosix(to.slice(sepPos + 1)); | |
| 40 | } else { | |
| 41 | return `.${toPosix(to.slice(sepPos))}`; | |
| 42 | } | |
| 43 | } |
src/file-viewer/redirects.ts created+284| ... | ... | @@ -0,0 +1,284 @@ |
| 1 | // ImportInjector | |
| 2 | class ImportInjector { | |
| 3 | constructor(path, importedSource, opts) { | |
| 4 | this._defaultOpts = { | |
| 5 | importedSource: null, | |
| 6 | importedType: "commonjs", | |
| 7 | importedInterop: "babel", | |
| 8 | importingInterop: "babel", | |
| 9 | ensureLiveReference: false, | |
| 10 | ensureNoContext: false, | |
| 11 | importPosition: "before" | |
| 12 | }; | |
| 13 | const programPath = path.find(p => p.isProgram()); | |
| 14 | this._programPath = programPath; | |
| 15 | this._programScope = programPath.scope; | |
| 16 | this._hub = programPath.hub; | |
| 17 | this._defaultOpts = this._applyDefaults(importedSource, opts, true); | |
| 18 | } | |
| 19 | addDefault(importedSourceIn, opts) { | |
| 20 | return this.addNamed("default", importedSourceIn, opts); | |
| 21 | } | |
| 22 | addNamed(importName, importedSourceIn, opts) { | |
| 23 | _assert(typeof importName === "string"); | |
| 24 | return this._generateImport(this._applyDefaults(importedSourceIn, opts), importName); | |
| 25 | } | |
| 26 | addNamespace(importedSourceIn, opts) { | |
| 27 | return this._generateImport(this._applyDefaults(importedSourceIn, opts), null); | |
| 28 | } | |
| 29 | addSideEffect(importedSourceIn, opts) { | |
| 30 | return this._generateImport(this._applyDefaults(importedSourceIn, opts), void 0); | |
| 31 | } | |
| 32 | _applyDefaults(importedSource, opts, isInit = false) { | |
| 33 | let newOpts; | |
| 34 | if (typeof importedSource === "string") { | |
| 35 | newOpts = Object.assign({}, this._defaultOpts, { | |
| 36 | importedSource | |
| 37 | }, opts); | |
| 38 | } else { | |
| 39 | _assert(!opts, "Unexpected secondary arguments."); | |
| 40 | newOpts = Object.assign({}, this._defaultOpts, importedSource); | |
| 41 | } | |
| 42 | if (!isInit && opts) { | |
| 43 | if (opts.nameHint !== undefined) newOpts.nameHint = opts.nameHint; | |
| 44 | if (opts.blockHoist !== undefined) newOpts.blockHoist = opts.blockHoist; | |
| 45 | } | |
| 46 | return newOpts; | |
| 47 | } | |
| 48 | _generateImport(opts, importName) { | |
| 49 | const isDefault = importName === "default"; | |
| 50 | const isNamed = !!importName && !isDefault; | |
| 51 | const isNamespace = importName === null; | |
| 52 | const { | |
| 53 | importedSource, | |
| 54 | importedType, | |
| 55 | importedInterop, | |
| 56 | importingInterop, | |
| 57 | ensureLiveReference, | |
| 58 | ensureNoContext, | |
| 59 | nameHint, | |
| 60 | importPosition, | |
| 61 | blockHoist | |
| 62 | } = opts; | |
| 63 | let name = nameHint || importName; | |
| 64 | const isMod = (0, _isModule.default)(this._programPath); | |
| 65 | const isModuleForNode = isMod && importingInterop === "node"; | |
| 66 | const isModuleForBabel = isMod && importingInterop === "babel"; | |
| 67 | if (importPosition === "after" && !isMod) { | |
| 68 | throw new Error(`"importPosition": "after" is only supported in modules`); | |
| 69 | } | |
| 70 | const builder = new _importBuilder.default(importedSource, this._programScope, this._hub); | |
| 71 | if (importedType === "es6") { | |
| 72 | if (!isModuleForNode && !isModuleForBabel) { | |
| 73 | throw new Error("Cannot import an ES6 module from CommonJS"); | |
| 74 | } | |
| 75 | builder.import(); | |
| 76 | if (isNamespace) { | |
| 77 | builder.namespace(nameHint || importedSource); | |
| 78 | } else if (isDefault || isNamed) { | |
| 79 | builder.named(name, importName); | |
| 80 | } | |
| 81 | } else if (importedType !== "commonjs") { | |
| 82 | throw new Error(`Unexpected interopType "${importedType}"`); | |
| 83 | } else if (importedInterop === "babel") { | |
| 84 | if (isModuleForNode) { | |
| 85 | name = name !== "default" ? name : importedSource; | |
| 86 | const es6Default = `${importedSource}$es6Default`; | |
| 87 | builder.import(); | |
| 88 | if (isNamespace) { | |
| 89 | builder.default(es6Default).var(name || importedSource).wildcardInterop(); | |
| 90 | } else if (isDefault) { | |
| 91 | if (ensureLiveReference) { | |
| 92 | builder.default(es6Default).var(name || importedSource).defaultInterop().read("default"); | |
| 93 | } else { | |
| 94 | builder.default(es6Default).var(name).defaultInterop().prop(importName); | |
| 95 | } | |
| 96 | } else if (isNamed) { | |
| 97 | builder.default(es6Default).read(importName); | |
| 98 | } | |
| 99 | } else if (isModuleForBabel) { | |
| 100 | builder.import(); | |
| 101 | if (isNamespace) { | |
| 102 | builder.namespace(name || importedSource); | |
| 103 | } else if (isDefault || isNamed) { | |
| 104 | builder.named(name, importName); | |
| 105 | } | |
| 106 | } else { | |
| 107 | builder.require(); | |
| 108 | if (isNamespace) { | |
| 109 | builder.var(name || importedSource).wildcardInterop(); | |
| 110 | } else if ((isDefault || isNamed) && ensureLiveReference) { | |
| 111 | if (isDefault) { | |
| 112 | name = name !== "default" ? name : importedSource; | |
| 113 | builder.var(name).read(importName); | |
| 114 | builder.defaultInterop(); | |
| 115 | } else { | |
| 116 | builder.var(importedSource).read(importName); | |
| 117 | } | |
| 118 | } else if (isDefault) { | |
| 119 | builder.var(name).defaultInterop().prop(importName); | |
| 120 | } else if (isNamed) { | |
| 121 | builder.var(name).prop(importName); | |
| 122 | } | |
| 123 | } | |
| 124 | } else if (importedInterop === "compiled") { | |
| 125 | if (isModuleForNode) { | |
| 126 | builder.import(); | |
| 127 | if (isNamespace) { | |
| 128 | builder.default(name || importedSource); | |
| 129 | } else if (isDefault || isNamed) { | |
| 130 | builder.default(importedSource).read(name); | |
| 131 | } | |
| 132 | } else if (isModuleForBabel) { | |
| 133 | builder.import(); | |
| 134 | if (isNamespace) { | |
| 135 | builder.namespace(name || importedSource); | |
| 136 | } else if (isDefault || isNamed) { | |
| 137 | builder.named(name, importName); | |
| 138 | } | |
| 139 | } else { | |
| 140 | builder.require(); | |
| 141 | if (isNamespace) { | |
| 142 | builder.var(name || importedSource); | |
| 143 | } else if (isDefault || isNamed) { | |
| 144 | if (ensureLiveReference) { | |
| 145 | builder.var(importedSource).read(name); | |
| 146 | } else { | |
| 147 | builder.prop(importName).var(name); | |
| 148 | } | |
| 149 | } | |
| 150 | } | |
| 151 | } else if (importedInterop === "uncompiled") { | |
| 152 | if (isDefault && ensureLiveReference) { | |
| 153 | throw new Error("No live reference for commonjs default"); | |
| 154 | } | |
| 155 | if (isModuleForNode) { | |
| 156 | builder.import(); | |
| 157 | if (isNamespace) { | |
| 158 | builder.default(name || importedSource); | |
| 159 | } else if (isDefault) { | |
| 160 | builder.default(name); | |
| 161 | } else if (isNamed) { | |
| 162 | builder.default(importedSource).read(name); | |
| 163 | } | |
| 164 | } else if (isModuleForBabel) { | |
| 165 | builder.import(); | |
| 166 | if (isNamespace) { | |
| 167 | builder.default(name || importedSource); | |
| 168 | } else if (isDefault) { | |
| 169 | builder.default(name); | |
| 170 | } else if (isNamed) { | |
| 171 | builder.named(name, importName); | |
| 172 | } | |
| 173 | } else { | |
| 174 | builder.require(); | |
| 175 | if (isNamespace) { | |
| 176 | builder.var(name || importedSource); | |
| 177 | } else if (isDefault) { | |
| 178 | builder.var(name); | |
| 179 | } else if (isNamed) { | |
| 180 | if (ensureLiveReference) { | |
| 181 | builder.var(importedSource).read(name); | |
| 182 | } else { | |
| 183 | builder.var(name).prop(importName); | |
| 184 | } | |
| 185 | } | |
| 186 | } | |
| 187 | } else { | |
| 188 | throw new Error(`Unknown importedInterop "${importedInterop}".`); | |
| 189 | } | |
| 190 | const { | |
| 191 | statements, | |
| 192 | resultName | |
| 193 | } = builder.done(); | |
| 194 | this._insertStatements(statements, importPosition, blockHoist); | |
| 195 | if ((isDefault || isNamed) && ensureNoContext && resultName.type !== "Identifier") { | |
| 196 | return sequenceExpression([numericLiteral(0), resultName]); | |
| 197 | } | |
| 198 | return resultName; | |
| 199 | } | |
| 200 | _insertStatements(statements, importPosition = "before", blockHoist = 3) { | |
| 201 | if (importPosition === "after") { | |
| 202 | if (this._insertStatementsAfter(statements)) return; | |
| 203 | } else { | |
| 204 | if (this._insertStatementsBefore(statements, blockHoist)) return; | |
| 205 | } | |
| 206 | this._programPath.unshiftContainer("body", statements); | |
| 207 | } | |
| 208 | _insertStatementsBefore(statements, blockHoist) { | |
| 209 | if (statements.length === 1 && isImportDeclaration(statements[0]) && isValueImport(statements[0])) { | |
| 210 | const firstImportDecl = this._programPath.get("body").find(p => { | |
| 211 | return p.isImportDeclaration() && isValueImport(p.node); | |
| 212 | }); | |
| 213 | if ((firstImportDecl == null ? void 0 : firstImportDecl.node.source.value) === statements[0].source.value && maybeAppendImportSpecifiers(firstImportDecl.node, statements[0])) { | |
| 214 | return true; | |
| 215 | } | |
| 216 | } | |
| 217 | statements.forEach(node => { | |
| 218 | node._blockHoist = blockHoist; | |
| 219 | }); | |
| 220 | const targetPath = this._programPath.get("body").find(p => { | |
| 221 | const val = p.node._blockHoist; | |
| 222 | return Number.isFinite(val) && val < 4; | |
| 223 | }); | |
| 224 | if (targetPath) { | |
| 225 | targetPath.insertBefore(statements); | |
| 226 | return true; | |
| 227 | } | |
| 228 | return false; | |
| 229 | } | |
| 230 | _insertStatementsAfter(statements) { | |
| 231 | const statementsSet = new Set(statements); | |
| 232 | const importDeclarations = new Map(); | |
| 233 | for (const statement of statements) { | |
| 234 | if (isImportDeclaration(statement) && isValueImport(statement)) { | |
| 235 | const source = statement.source.value; | |
| 236 | if (!importDeclarations.has(source)) importDeclarations.set(source, []); | |
| 237 | importDeclarations.get(source).push(statement); | |
| 238 | } | |
| 239 | } | |
| 240 | let lastImportPath = null; | |
| 241 | for (const bodyStmt of this._programPath.get("body")) { | |
| 242 | if (bodyStmt.isImportDeclaration() && isValueImport(bodyStmt.node)) { | |
| 243 | lastImportPath = bodyStmt; | |
| 244 | const source = bodyStmt.node.source.value; | |
| 245 | const newImports = importDeclarations.get(source); | |
| 246 | if (!newImports) continue; | |
| 247 | for (const decl of newImports) { | |
| 248 | if (!statementsSet.has(decl)) continue; | |
| 249 | if (maybeAppendImportSpecifiers(bodyStmt.node, decl)) { | |
| 250 | statementsSet.delete(decl); | |
| 251 | } | |
| 252 | } | |
| 253 | } | |
| 254 | } | |
| 255 | if (statementsSet.size === 0) return true; | |
| 256 | if (lastImportPath) lastImportPath.insertAfter(Array.from(statementsSet)); | |
| 257 | return !!lastImportPath; | |
| 258 | } | |
| 259 | } | |
| 260 | ||
| 261 | // addDefault | |
| 262 | function addDefault(path, importedSource, opts) { | |
| 263 | return new _importInjector.default(path).addDefault(importedSource, opts); | |
| 264 | } | |
| 265 | ||
| 266 | // addNamed | |
| 267 | function addNamed(path, name, importedSource, opts) { | |
| 268 | return new _importInjector.default(path).addNamed(name, importedSource, opts); | |
| 269 | } | |
| 270 | ||
| 271 | // addNamespace | |
| 272 | function addNamespace(path, importedSource, opts) { | |
| 273 | return new _importInjector.default(path).addNamespace(importedSource, opts); | |
| 274 | } | |
| 275 | ||
| 276 | // addSideEffect | |
| 277 | function addSideEffect(path, importedSource, opts) { | |
| 278 | return new _importInjector.default(path).addSideEffect(importedSource, opts); | |
| 279 | } | |
| 280 | ||
| 281 | // isModule | |
| 282 | function isModule(path) { | |
| 283 | return path.node.sourceType === "module"; | |
| 284 | } |
src/file-viewer/scan.ts created+1008| ... | ... | @@ -0,0 +1,1008 @@ |
| 1 | // This file was started by AI and maintained by hand since. | |
| 2 | import "@paperclover/console/inject"; | |
| 3 | import { Progress } from "@paperclover/console/Progress"; | |
| 4 | import { Spinner } from "@paperclover/console/Spinner"; | |
| 5 | import assert from "node:assert"; | |
| 6 | import { execFile } from "node:child_process"; | |
| 7 | import { existsSync, Stats } from "node:fs"; | |
| 8 | import * as fsp from "node:fs/promises"; | |
| 9 | import * as path from "node:path"; | |
| 10 | import { promisify } from "node:util"; | |
| 11 | import { BlobAsset, cache, MediaFile } from "../db.ts"; | |
| 12 | import { formatDate, formatSize } from "./share.ts"; | |
| 13 | import { highlightCode, type Language } from "./highlight.ts"; | |
| 14 | import process from "node:process"; | |
| 15 | ||
| 16 | const execFileAsync = promisify(execFile); | |
| 17 | ||
| 18 | // Configuration | |
| 19 | const FILE_ROOT = process.env.SCAN_FILE_ROOT; | |
| 20 | if (!FILE_ROOT) { | |
| 21 | throw new Error( | |
| 22 | "FILE_ROOT environment variable not set (e.g. '/path/to/files')", | |
| 23 | ); | |
| 24 | } | |
| 25 | const LOCAL_DIR = path.resolve(FILE_ROOT); | |
| 26 | const DRY_RUN = process.argv.includes("--dry-run"); | |
| 27 | const SHOULD_COMPRESS = true; | |
| 28 | const VERBOSE = process.argv.includes("--verbose"); | |
| 29 | const SHOULD_SCRUB = true; | |
| 30 | const COMPRESS_STORE = process.env.COMPRESS_STORE || | |
| 31 | path.join(process.cwd(), ".clover/compressed"); | |
| 32 | ||
| 33 | // Helper function for logging that respects verbose flag | |
| 34 | function log(message: string, always = false): void { | |
| 35 | if (always || VERBOSE) { | |
| 36 | console.log(message); | |
| 37 | } | |
| 38 | } | |
| 39 | ||
| 40 | // File extensions that need duration metadata | |
| 41 | const MEDIA_EXTENSIONS = new Set([ | |
| 42 | ".mp4", | |
| 43 | ".mkv", | |
| 44 | ".webm", | |
| 45 | ".avi", | |
| 46 | ".mov", | |
| 47 | ".mp3", | |
| 48 | ".flac", | |
| 49 | ".wav", | |
| 50 | ".ogg", | |
| 51 | ".m4a", | |
| 52 | ]); | |
| 53 | ||
| 54 | // File extensions that need dimension metadata | |
| 55 | const IMAGE_EXTENSIONS = new Set([ | |
| 56 | ".jpg", | |
| 57 | ".jpeg", | |
| 58 | ".png", | |
| 59 | ".gif", | |
| 60 | ".webp", | |
| 61 | ".avif", | |
| 62 | ".heic", | |
| 63 | ".svg", | |
| 64 | ]); | |
| 65 | ||
| 66 | const VIDEO_EXTENSIONS = new Set([".mp4", ".mkv", ".webm", ".avi", ".mov"]); | |
| 67 | ||
| 68 | // File extensions that need metadata scrubbing | |
| 69 | const SCRUB_EXTENSIONS = new Set([ | |
| 70 | ".jpg", | |
| 71 | ".jpeg", | |
| 72 | ".png", | |
| 73 | ".mov", | |
| 74 | ".mp4", | |
| 75 | ".m4a", | |
| 76 | ]); | |
| 77 | ||
| 78 | const CODE_EXTENSIONS: Record<string, Language> = { | |
| 79 | ".json": "json", | |
| 80 | ".toml": "toml", | |
| 81 | ".ts": "ts", | |
| 82 | ".js": "ts", | |
| 83 | ".tsx": "tsx", | |
| 84 | ".jsx": "tsx", | |
| 85 | ".css": "css", | |
| 86 | ".py": "python", | |
| 87 | ".lua": "lua", | |
| 88 | ".sh": "shell", | |
| 89 | ".bat": "dosbatch", | |
| 90 | ".ps1": "powershell", | |
| 91 | ".cmd": "dosbatch", | |
| 92 | ".yaml": "yaml", | |
| 93 | ".yml": "yaml", | |
| 94 | ".zig": "zig", | |
| 95 | ".astro": "astro", | |
| 96 | ".mdx": "mdx", | |
| 97 | ".xml": "xml", | |
| 98 | ".jsonc": "json", | |
| 99 | ".php": "php", | |
| 100 | ".patch": "diff", | |
| 101 | ".diff": "diff", | |
| 102 | }; | |
| 103 | ||
| 104 | const READ_CONTENTS_EXTENSIONS = new Set([".txt", ".chat"]); | |
| 105 | ||
| 106 | // For files that have changed indexing logic, update the date here rescanning | |
| 107 | // will reconstruct the entire file object. This way you can incrementally | |
| 108 | // update new file types without having to reindex everything. | |
| 109 | const lastUpdateTypes: Record<string, Date> = {}; | |
| 110 | lastUpdateTypes[".lnk"] = new Date("2025-05-13 13:58:00"); | |
| 111 | for (const ext in CODE_EXTENSIONS) { | |
| 112 | lastUpdateTypes[ext] = new Date("2025-05-13 13:58:00"); | |
| 113 | } | |
| 114 | for (const ext of READ_CONTENTS_EXTENSIONS) { | |
| 115 | lastUpdateTypes[ext] = new Date("2025-05-13 13:58:00"); | |
| 116 | } | |
| 117 | lastUpdateTypes[".diff"] = new Date("2025-05-18 13:58:00"); | |
| 118 | lastUpdateTypes[".patch"] = new Date("2025-05-18 13:58:00"); | |
| 119 | ||
| 120 | // Helper functions for metadata extraction | |
| 121 | async function calculateHash(filePath: string): Promise<string> { | |
| 122 | try { | |
| 123 | const hash = await execFileAsync("sha1sum", [filePath]); | |
| 124 | return hash.stdout.split(" ")[0]; | |
| 125 | } catch (error) { | |
| 126 | console.error(`Error calculating hash for ${filePath}:`, error); | |
| 127 | throw error; | |
| 128 | } | |
| 129 | } | |
| 130 | ||
| 131 | async function calculateDuration(filePath: string): Promise<number> { | |
| 132 | try { | |
| 133 | const ext = path.extname(filePath).toLowerCase(); | |
| 134 | if (!MEDIA_EXTENSIONS.has(ext)) return 0; | |
| 135 | ||
| 136 | const { stdout } = await execFileAsync("ffprobe", [ | |
| 137 | "-v", | |
| 138 | "error", | |
| 139 | "-show_entries", | |
| 140 | "format=duration", | |
| 141 | "-of", | |
| 142 | "default=noprint_wrappers=1:nokey=1", | |
| 143 | filePath, | |
| 144 | ]); | |
| 145 | return Math.ceil(parseFloat(stdout.trim())); | |
| 146 | } catch (error) { | |
| 147 | console.error(`Error calculating duration for ${filePath}:`, error); | |
| 148 | return 0; // Return 0 for duration on error | |
| 149 | } | |
| 150 | } | |
| 151 | ||
| 152 | async function calculateDimensions(filePath: string): Promise<string> { | |
| 153 | const ext = path.extname(filePath).toLowerCase(); | |
| 154 | if (!IMAGE_EXTENSIONS.has(ext) && !VIDEO_EXTENSIONS.has(ext)) return ""; | |
| 155 | ||
| 156 | try { | |
| 157 | if (ext === ".svg") { | |
| 158 | // For SVG files, parse the file and extract width/height | |
| 159 | const content = await fsp.readFile(filePath, "utf8"); | |
| 160 | const widthMatch = content.match(/width="(\d+)"/); | |
| 161 | const heightMatch = content.match(/height="(\d+)"/); | |
| 162 | ||
| 163 | if (widthMatch && heightMatch) { | |
| 164 | return `${widthMatch[1]}x${heightMatch[1]}`; | |
| 165 | } | |
| 166 | } else if (IMAGE_EXTENSIONS.has(ext) || VIDEO_EXTENSIONS.has(ext)) { | |
| 167 | // Use ffprobe for images and videos | |
| 168 | const { stdout } = await execFileAsync("ffprobe", [ | |
| 169 | "-v", | |
| 170 | "error", | |
| 171 | "-select_streams", | |
| 172 | "v:0", | |
| 173 | "-show_entries", | |
| 174 | "stream=width,height", | |
| 175 | "-of", | |
| 176 | "csv=s=x:p=0", | |
| 177 | filePath, | |
| 178 | ]); | |
| 179 | return stdout.trim(); | |
| 180 | } | |
| 181 | } catch (error) { | |
| 182 | console.error(`Error calculating dimensions for ${filePath}:`, error); | |
| 183 | } | |
| 184 | ||
| 185 | return ""; | |
| 186 | } | |
| 187 | ||
| 188 | // Helper function to check and remove location metadata | |
| 189 | async function scrubLocationMetadata( | |
| 190 | filePath: string, | |
| 191 | stats: Stats, | |
| 192 | ): Promise<boolean> { | |
| 193 | try { | |
| 194 | const ext = path.extname(filePath).toLowerCase(); | |
| 195 | if (!SCRUB_EXTENSIONS.has(ext)) return false; | |
| 196 | ||
| 197 | let hasLocation = false; | |
| 198 | let args: string[] = []; | |
| 199 | ||
| 200 | // Check for location metadata based on file type | |
| 201 | const tempOutput = path.join( | |
| 202 | path.dirname(filePath), | |
| 203 | `._${path.basename(filePath)}.temp`, | |
| 204 | ); | |
| 205 | switch (ext) { | |
| 206 | case ".jpg": | |
| 207 | case ".jpeg": | |
| 208 | case ".png": | |
| 209 | // Check for GPS tags in EXIF | |
| 210 | const { stdout: gpsCheck } = await execFileAsync("exiftool", [ | |
| 211 | "-gps:all", | |
| 212 | filePath, | |
| 213 | ]); | |
| 214 | hasLocation = gpsCheck.trim().length > 0; | |
| 215 | args = ["-gps:all=", filePath, "-o", tempOutput]; | |
| 216 | break; | |
| 217 | case ".mov": | |
| 218 | case ".mp4": | |
| 219 | // Check for GPS metadata in video files | |
| 220 | const { stdout: videoCheck } = await execFileAsync("exiftool", [ | |
| 221 | "-ee", | |
| 222 | "-G3", | |
| 223 | "-s", | |
| 224 | filePath, | |
| 225 | ]); | |
| 226 | hasLocation = videoCheck.includes("GPS") || | |
| 227 | videoCheck.includes("Location"); | |
| 228 | args = ["-gps:all=", "-xmp:all=", filePath, "-o", tempOutput]; | |
| 229 | break; | |
| 230 | case ".m4a": | |
| 231 | // Check for location and other metadata in m4a files | |
| 232 | const { stdout: m4aCheck } = await execFileAsync("exiftool", [ | |
| 233 | "-ee", | |
| 234 | "-G3", | |
| 235 | "-s", | |
| 236 | filePath, | |
| 237 | ]); | |
| 238 | hasLocation = m4aCheck.includes("GPS") || | |
| 239 | m4aCheck.includes("Location") || | |
| 240 | m4aCheck.includes("Filename") || | |
| 241 | m4aCheck.includes("Title"); | |
| 242 | ||
| 243 | if (hasLocation) { | |
| 244 | args = [ | |
| 245 | "-gps:all=", | |
| 246 | "-location:all=", | |
| 247 | "-filename:all=", | |
| 248 | "-title=", | |
| 249 | "-m4a:all=", | |
| 250 | filePath, | |
| 251 | "-o", | |
| 252 | tempOutput, | |
| 253 | ]; | |
| 254 | } | |
| 255 | break; | |
| 256 | } | |
| 257 | ||
| 258 | const accessTime = stats.atime; | |
| 259 | const modTime = stats.mtime; | |
| 260 | ||
| 261 | let backup: string | null = null; | |
| 262 | try { | |
| 263 | if (hasLocation) { | |
| 264 | if (DRY_RUN) return true; | |
| 265 | ||
| 266 | // Prepare a backup | |
| 267 | const tmp = path.join( | |
| 268 | path.dirname(filePath), | |
| 269 | `._${path.basename(filePath)}.backup`, | |
| 270 | ); | |
| 271 | await fsp.copyFile(filePath, tmp); | |
| 272 | await fsp.utimes(tmp, accessTime, modTime); | |
| 273 | backup = tmp; | |
| 274 | ||
| 275 | // Remove metadata | |
| 276 | await execFileAsync("exiftool", args); | |
| 277 | if (!existsSync(tempOutput)) { | |
| 278 | throw new Error(`Failed to create output file: ${tempOutput}`); | |
| 279 | } | |
| 280 | ||
| 281 | // Restore original timestamps | |
| 282 | await fsp.rename(tempOutput, filePath); | |
| 283 | await fsp.utimes(filePath, accessTime, modTime); | |
| 284 | ||
| 285 | // Backup is no longer needed | |
| 286 | await fsp.unlink(backup); | |
| 287 | ||
| 288 | log( | |
| 289 | `Scrubbed location metadata in ${path.relative(LOCAL_DIR, filePath)}`, | |
| 290 | true, | |
| 291 | ); | |
| 292 | return true; | |
| 293 | } | |
| 294 | } catch (error) { | |
| 295 | if (backup) { | |
| 296 | await fsp.rename(backup, filePath); | |
| 297 | } | |
| 298 | if (existsSync(tempOutput)) { | |
| 299 | await fsp.unlink(tempOutput); | |
| 300 | } | |
| 301 | throw error; | |
| 302 | } | |
| 303 | } catch (error) { | |
| 304 | console.error(`Error scrubbing metadata for ${filePath}:`, error); | |
| 305 | } | |
| 306 | ||
| 307 | return false; | |
| 308 | } | |
| 309 | ||
| 310 | // Queue implementation for parallel processing | |
| 311 | type AsyncQueueProcessor<T> = (s: Spinner, item: T) => Promise<void>; | |
| 312 | class AsyncQueue<T> { | |
| 313 | private queue: T[] = []; | |
| 314 | private running = 0; | |
| 315 | private maxConcurrent: number; | |
| 316 | private processed = 0; | |
| 317 | private progress?: Progress<{ active: Spinner[] }>; | |
| 318 | private name: string; | |
| 319 | private estimate?: number; | |
| 320 | ||
| 321 | constructor(name: string, maxConcurrent: number) { | |
| 322 | this.maxConcurrent = maxConcurrent; | |
| 323 | this.name = name; | |
| 324 | } | |
| 325 | ||
| 326 | setEstimate(estimate: number) { | |
| 327 | this.estimate = estimate; | |
| 328 | if (this.progress) { | |
| 329 | this.progress.total = Math.max( | |
| 330 | this.processed + this.queue.length, | |
| 331 | estimate, | |
| 332 | ); | |
| 333 | } | |
| 334 | } | |
| 335 | ||
| 336 | getProgress() { | |
| 337 | if (!this.progress) { | |
| 338 | this.progress = new Progress({ | |
| 339 | spinner: null, | |
| 340 | text: ({ active }) => { | |
| 341 | const now = performance.now(); | |
| 342 | let text = `[${this.processed}/${ | |
| 343 | this.processed + this.queue.length | |
| 344 | }] ${this.name}`; | |
| 345 | let n = 0; | |
| 346 | for (const item of active) { | |
| 347 | let itemText = "- " + item.format(now); | |
| 348 | text += `\n` + | |
| 349 | itemText.slice(0, Math.max(0, process.stdout.columns - 1)); | |
| 350 | if (n > 10) { | |
| 351 | text += `\n ... + ${active.length - n} more`; | |
| 352 | break; | |
| 353 | } | |
| 354 | n++; | |
| 355 | } | |
| 356 | return text; | |
| 357 | }, | |
| 358 | props: { | |
| 359 | active: [] as Spinner[], | |
| 360 | }, | |
| 361 | }); | |
| 362 | this.progress.total = this.estimate ?? 0; | |
| 363 | this.progress.value = 0; | |
| 364 | this.progress.fps = 30; | |
| 365 | } | |
| 366 | return this.progress; | |
| 367 | } | |
| 368 | ||
| 369 | async add(item: T, processor: AsyncQueueProcessor<T>): Promise<void> { | |
| 370 | this.queue.push(item); | |
| 371 | this.getProgress().total = Math.max( | |
| 372 | this.processed + this.queue.length, | |
| 373 | this.estimate ?? 0, | |
| 374 | ); | |
| 375 | return this.processNext(processor); | |
| 376 | } | |
| 377 | ||
| 378 | async addBatch(items: T[], processor: AsyncQueueProcessor<T>): Promise<void> { | |
| 379 | this.queue.push(...items); | |
| 380 | this.getProgress().total = Math.max( | |
| 381 | this.processed + this.queue.length, | |
| 382 | this.estimate ?? 0, | |
| 383 | ); | |
| 384 | return this.processNext(processor); | |
| 385 | } | |
| 386 | ||
| 387 | private async processNext(processor: AsyncQueueProcessor<T>): Promise<void> { | |
| 388 | if (this.running >= this.maxConcurrent || this.queue.length === 0) { | |
| 389 | return; | |
| 390 | } | |
| 391 | ||
| 392 | const item = this.queue.shift(); | |
| 393 | if (!item) return; | |
| 394 | ||
| 395 | this.running++; | |
| 396 | ||
| 397 | try { | |
| 398 | const progress = this.getProgress(); | |
| 399 | ||
| 400 | let itemText = ""; | |
| 401 | if (typeof item === "string") { | |
| 402 | itemText = item; | |
| 403 | } else if (typeof item === "object" && item !== null && "path" in item) { | |
| 404 | itemText = "" + item.path; | |
| 405 | } else { | |
| 406 | itemText = JSON.stringify(item); | |
| 407 | } | |
| 408 | if (itemText.startsWith(LOCAL_DIR)) { | |
| 409 | itemText = path.relative(LOCAL_DIR, itemText); | |
| 410 | } | |
| 411 | ||
| 412 | const spinner = new Spinner(itemText); | |
| 413 | spinner.stop(); | |
| 414 | progress.props.active.unshift(spinner); | |
| 415 | await processor(spinner, item); | |
| 416 | progress.props = { | |
| 417 | active: progress.props.active.filter((s) => s !== spinner), | |
| 418 | }; | |
| 419 | this.processed++; | |
| 420 | progress.value = this.processed; | |
| 421 | } catch (error) { | |
| 422 | console.error(`Error processing ${this.name} queue item:`, error); | |
| 423 | this.processed++; | |
| 424 | this.getProgress().value = this.processed; | |
| 425 | } finally { | |
| 426 | this.running--; | |
| 427 | await this.processNext(processor); | |
| 428 | } | |
| 429 | } | |
| 430 | ||
| 431 | async waitForCompletion(): Promise<void> { | |
| 432 | if (this.queue.length === 0 && this.running === 0) { | |
| 433 | if (this.processed > 0) { | |
| 434 | this.#success(); | |
| 435 | } | |
| 436 | return; | |
| 437 | } | |
| 438 | ||
| 439 | return new Promise((resolve) => { | |
| 440 | const checkInterval = setInterval(() => { | |
| 441 | if (this.queue.length === 0 && this.running === 0) { | |
| 442 | clearInterval(checkInterval); | |
| 443 | this.#success(); | |
| 444 | resolve(); | |
| 445 | } | |
| 446 | }, 100); | |
| 447 | }); | |
| 448 | } | |
| 449 | ||
| 450 | #success() { | |
| 451 | this.getProgress().success(`${this.processed} ${this.name}`); | |
| 452 | } | |
| 453 | } | |
| 454 | ||
| 455 | function skipBasename(basename: string): boolean { | |
| 456 | // dot files must be incrementally tracked | |
| 457 | if (basename === ".dirsort") return true; | |
| 458 | ||
| 459 | return ( | |
| 460 | basename.startsWith(".") || | |
| 461 | basename.startsWith("._") || | |
| 462 | basename === ".DS_Store" || | |
| 463 | basename.toLowerCase() === "thumbs.db" || | |
| 464 | basename.toLowerCase() === "desktop.ini" | |
| 465 | ); | |
| 466 | } | |
| 467 | ||
| 468 | // File system scanner | |
| 469 | class FileSystemScanner { | |
| 470 | private visitedPaths = new Set<string>(); | |
| 471 | private previousPaths = new Set<string>(); | |
| 472 | private dirQueue = new AsyncQueue<string>("Scan Directories", 10); | |
| 473 | private fileQueue = new AsyncQueue<{ path: string; stat: any }>( | |
| 474 | "File metadata", | |
| 475 | 20, | |
| 476 | ); | |
| 477 | private compressQueue: AsyncQueue<{ file: MediaFile; path: string }> | null = | |
| 478 | SHOULD_COMPRESS ? new AsyncQueue("Compress Assets", 10) : null; | |
| 479 | ||
| 480 | private getDbPath(localPath: string): string { | |
| 481 | // Convert local file system path to database path | |
| 482 | const relativePath = path.relative(LOCAL_DIR, localPath); | |
| 483 | return "/" + relativePath.split(path.sep).join(path.posix.sep); | |
| 484 | } | |
| 485 | ||
| 486 | private getLocalPath(dbPath: string): string { | |
| 487 | // Convert database path to local file system path | |
| 488 | return path.join(LOCAL_DIR, dbPath.slice(1)); | |
| 489 | } | |
| 490 | ||
| 491 | async scanFile(s: Spinner, filePath: string, stat: any): Promise<void> { | |
| 492 | const dbPath = this.getDbPath(filePath); | |
| 493 | ||
| 494 | // Skip hidden files | |
| 495 | const basename = path.basename(filePath); | |
| 496 | if (skipBasename(basename)) { | |
| 497 | return; | |
| 498 | } | |
| 499 | ||
| 500 | this.visitedPaths.add(dbPath); | |
| 501 | ||
| 502 | // Get existing file info from db | |
| 503 | const existingFile = MediaFile.getByPath(dbPath); | |
| 504 | ||
| 505 | // Determine which date to use (for date protection) | |
| 506 | let dateToUse = stat.mtime; | |
| 507 | const year2025Start = new Date("2025-01-01T00:00:00Z"); | |
| 508 | ||
| 509 | if ( | |
| 510 | existingFile && | |
| 511 | existingFile.date < year2025Start && | |
| 512 | stat.mtime >= year2025Start | |
| 513 | ) { | |
| 514 | console.error( | |
| 515 | `Error: ${dbPath} is ${ | |
| 516 | formatDate( | |
| 517 | existingFile.date, | |
| 518 | ) | |
| 519 | }, got modified to ${formatDate(stat.mtime)}`, | |
| 520 | ); | |
| 521 | dateToUse = existingFile.date; | |
| 522 | } | |
| 523 | ||
| 524 | // Check if we need to reprocess the file | |
| 525 | if (existingFile && existingFile.size === stat.size && existingFile.hash) { | |
| 526 | maybe_skip: { | |
| 527 | const lastUpdateDate = lastUpdateTypes[path.extname(filePath)]; | |
| 528 | if (lastUpdateDate && existingFile.lastUpdateDate < lastUpdateDate) { | |
| 529 | console.log( | |
| 530 | `Reprocessing ${dbPath} because indexing logic changed after ${ | |
| 531 | formatDate( | |
| 532 | lastUpdateDate, | |
| 533 | ) | |
| 534 | }`, | |
| 535 | ); | |
| 536 | break maybe_skip; | |
| 537 | } | |
| 538 | ||
| 539 | if (SHOULD_COMPRESS && existingFile.processed !== 2) { | |
| 540 | this.compressQueue!.add( | |
| 541 | { file: existingFile, path: dbPath }, | |
| 542 | this.compressFile.bind(this), | |
| 543 | ); | |
| 544 | } | |
| 545 | ||
| 546 | // File hasn't changed, no need to reprocess | |
| 547 | MediaFile.createFile({ | |
| 548 | path: dbPath, | |
| 549 | date: dateToUse, | |
| 550 | hash: existingFile.hash, | |
| 551 | size: stat.size, | |
| 552 | duration: existingFile.duration, | |
| 553 | dimensions: existingFile.dimensions, | |
| 554 | content: existingFile.contents, | |
| 555 | }); | |
| 556 | return; | |
| 557 | } | |
| 558 | } | |
| 559 | ||
| 560 | // Process the file | |
| 561 | log(`Processing file: ${dbPath}`); | |
| 562 | ||
| 563 | // Scrub location metadata if needed | |
| 564 | if (SHOULD_SCRUB) { | |
| 565 | if (await scrubLocationMetadata(filePath, stat)) { | |
| 566 | // Re-stat the file in case it was modified | |
| 567 | const newStat = await fsp.stat(filePath); | |
| 568 | stat.size = newStat.size; | |
| 569 | } | |
| 570 | } | |
| 571 | ||
| 572 | // Extract content | |
| 573 | const hash = await calculateHash(filePath); | |
| 574 | let content = ""; | |
| 575 | if (filePath.endsWith(".lnk")) { | |
| 576 | content = (await fsp.readFile(filePath, "utf8")).trim(); | |
| 577 | } | |
| 578 | const language = CODE_EXTENSIONS[path.extname(filePath)]; | |
| 579 | if (language) { | |
| 580 | read_code: { | |
| 581 | // An issue is that .ts is an overloaded extension, shared between | |
| 582 | // 'transport stream' and 'typescript'. | |
| 583 | // | |
| 584 | // Filter used here is: | |
| 585 | // - more than 1mb | |
| 586 | // - invalid UTF-8 | |
| 587 | if (stat.size > 1_000_000) break read_code; | |
| 588 | let code; | |
| 589 | const buf = await fsp.readFile(filePath); | |
| 590 | try { | |
| 591 | code = new TextDecoder("utf-8", { fatal: true }).decode(buf); | |
| 592 | } catch (error) { | |
| 593 | break read_code; | |
| 594 | } | |
| 595 | content = await highlightCode(code, language); | |
| 596 | } | |
| 597 | } | |
| 598 | if (!content && READ_CONTENTS_EXTENSIONS.has(path.extname(filePath))) { | |
| 599 | content = await fsp.readFile(filePath, "utf8"); | |
| 600 | } | |
| 601 | // End extract content | |
| 602 | ||
| 603 | if (hash === existingFile?.hash) { | |
| 604 | MediaFile.createFile({ | |
| 605 | path: dbPath, | |
| 606 | date: dateToUse, | |
| 607 | hash, | |
| 608 | size: stat.size, | |
| 609 | duration: existingFile.duration, | |
| 610 | dimensions: existingFile.dimensions, | |
| 611 | content, | |
| 612 | }); | |
| 613 | return; | |
| 614 | } else if (existingFile) { | |
| 615 | if (existingFile.processed === 2) { | |
| 616 | if (BlobAsset.decrementOrDelete(existingFile.hash)) { | |
| 617 | log( | |
| 618 | `Deleted compressed asset ${existingFile.hash}.{gzip, zstd}`, | |
| 619 | true, | |
| 620 | ); | |
| 621 | await fsp.unlink( | |
| 622 | path.join( | |
| 623 | COMPRESS_STORE, | |
| 624 | existingFile.hash.substring(0, 2), | |
| 625 | existingFile.hash + ".gz", | |
| 626 | ), | |
| 627 | ); | |
| 628 | await fsp.unlink( | |
| 629 | path.join( | |
| 630 | COMPRESS_STORE, | |
| 631 | existingFile.hash.substring(0, 2), | |
| 632 | existingFile.hash + ".zstd", | |
| 633 | ), | |
| 634 | ); | |
| 635 | } | |
| 636 | } | |
| 637 | } | |
| 638 | const [duration, dimensions] = await Promise.all([ | |
| 639 | calculateDuration(filePath), | |
| 640 | calculateDimensions(filePath), | |
| 641 | ]); | |
| 642 | ||
| 643 | // Update database with all metadata | |
| 644 | MediaFile.createFile({ | |
| 645 | path: dbPath, | |
| 646 | date: dateToUse, | |
| 647 | hash, | |
| 648 | size: stat.size, | |
| 649 | duration, | |
| 650 | dimensions, | |
| 651 | content, | |
| 652 | }); | |
| 653 | ||
| 654 | if (SHOULD_COMPRESS) { | |
| 655 | this.compressQueue!.add( | |
| 656 | { | |
| 657 | file: MediaFile.getByPath(dbPath)!, | |
| 658 | path: dbPath, | |
| 659 | }, | |
| 660 | this.compressFile.bind(this), | |
| 661 | ); | |
| 662 | } | |
| 663 | } | |
| 664 | ||
| 665 | async compressFile(s: Spinner, { file }: { file: MediaFile }): Promise<void> { | |
| 666 | log(`Compressing file: ${file.path}`); | |
| 667 | if (DRY_RUN) return; | |
| 668 | ||
| 669 | const filePath = path.join(FILE_ROOT!, file.path); | |
| 670 | ||
| 671 | const hash = file.hash; | |
| 672 | const firstTwoChars = hash.substring(0, 2); | |
| 673 | const compressDir = `${COMPRESS_STORE}/${firstTwoChars}`; | |
| 674 | const compressPath = `${compressDir}/${hash}`; | |
| 675 | ||
| 676 | // Create directory structure if it doesn't exist | |
| 677 | await fsp.mkdir(compressDir, { recursive: true }); | |
| 678 | ||
| 679 | // Compress the file with gzip | |
| 680 | const blob = BlobAsset.putOrIncrement(hash); | |
| 681 | if (blob.refs > 1) { | |
| 682 | log( | |
| 683 | `Skipping compression of ${filePath} because it already exists in ${compressPath}`, | |
| 684 | ); | |
| 685 | return; | |
| 686 | } | |
| 687 | // Check if already exists | |
| 688 | if (existsSync(compressPath + ".gz")) { | |
| 689 | file.setCompressed(true); | |
| 690 | return; | |
| 691 | } | |
| 692 | try { | |
| 693 | const gzipProcess = Bun.spawn(["gzip", "-c", filePath, "-9"], { | |
| 694 | stdout: Bun.file(compressPath + ".gz"), | |
| 695 | }); | |
| 696 | const zstdProcess = Bun.spawn(["zstd", "-c", filePath, "-9"], { | |
| 697 | stdout: Bun.file(compressPath + ".zstd"), | |
| 698 | }); | |
| 699 | const [gzipExited, zstdExited] = await Promise.all([ | |
| 700 | gzipProcess.exited, | |
| 701 | zstdProcess.exited, | |
| 702 | ]); | |
| 703 | assert(gzipExited === 0); | |
| 704 | assert(zstdExited === 0); | |
| 705 | assert(existsSync(compressPath + ".gz")); | |
| 706 | assert(existsSync(compressPath + ".zstd")); | |
| 707 | file.setCompressed(true); | |
| 708 | } catch (error) { | |
| 709 | console.error(`Error compressing file ${filePath}:`, error); | |
| 710 | BlobAsset.decrementOrDelete(hash); | |
| 711 | file.setCompressed(false); | |
| 712 | } | |
| 713 | } | |
| 714 | ||
| 715 | async scanDirectory(s: Spinner, dirPath: string): Promise<void> { | |
| 716 | const dbPath = this.getDbPath(dirPath); | |
| 717 | ||
| 718 | this.visitedPaths.add(dbPath); | |
| 719 | ||
| 720 | // Create or update directory entry | |
| 721 | log(`Scanning directory: ${dbPath}`); | |
| 722 | if (!DRY_RUN) { | |
| 723 | MediaFile.createOrUpdateDirectory(dbPath); | |
| 724 | } | |
| 725 | ||
| 726 | try { | |
| 727 | const entries = await fsp.readdir(dirPath, { withFileTypes: true }); | |
| 728 | ||
| 729 | // Process files and subdirectories | |
| 730 | for (const entry of entries) { | |
| 731 | const entryPath = path.join(dirPath, entry.name); | |
| 732 | ||
| 733 | // Skip hidden files and system files | |
| 734 | if (skipBasename(entry.name)) { | |
| 735 | continue; | |
| 736 | } | |
| 737 | ||
| 738 | if (entry.isDirectory()) { | |
| 739 | // Queue subdirectory for scanning | |
| 740 | this.dirQueue.add(entryPath, this.scanDirectory.bind(this)); | |
| 741 | } else if (entry.isFile()) { | |
| 742 | // Queue file for processing | |
| 743 | const stat = await fsp.stat(entryPath); | |
| 744 | ||
| 745 | this.fileQueue.add( | |
| 746 | { path: entryPath, stat }, | |
| 747 | async (s, item) => await this.scanFile(s, item.path, item.stat), | |
| 748 | ); | |
| 749 | } | |
| 750 | } | |
| 751 | } catch (error) { | |
| 752 | console.error(`Error scanning directory ${dirPath}:`, error); | |
| 753 | } | |
| 754 | } | |
| 755 | ||
| 756 | async processDirectoryMetadata(dirPath: string): Promise<void> { | |
| 757 | const dbPath = this.getDbPath(dirPath); | |
| 758 | const dir = MediaFile.getByPath(dbPath); | |
| 759 | ||
| 760 | if (!dir || dir.kind !== MediaFile.Kind.directory) { | |
| 761 | return; | |
| 762 | } | |
| 763 | ||
| 764 | if (DRY_RUN) return; | |
| 765 | ||
| 766 | const children = dir.getChildren(); | |
| 767 | ||
| 768 | // Calculate directory metadata | |
| 769 | let totalSize = 0; | |
| 770 | let newestDate = new Date(0); | |
| 771 | let allHashes = ""; | |
| 772 | ||
| 773 | // Check for readme.txt | |
| 774 | let readmeContent = ""; | |
| 775 | ||
| 776 | try { | |
| 777 | readmeContent = await fsp.readFile( | |
| 778 | path.join(dirPath, "readme.txt"), | |
| 779 | "utf8", | |
| 780 | ); | |
| 781 | } catch (error: any) { | |
| 782 | console.info(`no readme ${dirPath}`); | |
| 783 | if (error.code !== "ENOENT") { | |
| 784 | console.error(`Error reading readme.txt in ${dirPath}:`, error); | |
| 785 | } | |
| 786 | } | |
| 787 | ||
| 788 | let dirsort: string[] | null = null; | |
| 789 | try { | |
| 790 | dirsort = (await fsp.readFile(path.join(dirPath, ".dirsort"), "utf8")) | |
| 791 | .split("\n") | |
| 792 | .map((x) => x.trim()) | |
| 793 | .filter(Boolean); | |
| 794 | } catch (error: any) { | |
| 795 | if (error.code !== "ENOENT") { | |
| 796 | console.error(`Error reading .dirsort in ${dirPath}:`, error); | |
| 797 | } | |
| 798 | } | |
| 799 | ||
| 800 | // Process children | |
| 801 | for (const child of children) { | |
| 802 | totalSize += child.size; | |
| 803 | allHashes += child.hash; | |
| 804 | ||
| 805 | // Update newest date, ignoring readme.txt | |
| 806 | if (!child.path.endsWith("/readme.txt") && child.date > newestDate) { | |
| 807 | newestDate = child.date; | |
| 808 | } | |
| 809 | } | |
| 810 | ||
| 811 | // Create a hash for the directory | |
| 812 | const dirHash = new Bun.CryptoHasher("sha1") | |
| 813 | .update(dbPath + allHashes) | |
| 814 | .digest("hex"); | |
| 815 | ||
| 816 | // Update directory metadata | |
| 817 | MediaFile.markDirectoryProcessed({ | |
| 818 | id: dir.id, | |
| 819 | timestamp: newestDate, | |
| 820 | contents: readmeContent, | |
| 821 | size: totalSize, | |
| 822 | hash: dirHash, | |
| 823 | dirsort, | |
| 824 | }); | |
| 825 | } | |
| 826 | ||
| 827 | async findDeletedFiles(): Promise<void> { | |
| 828 | if (DRY_RUN) return; | |
| 829 | ||
| 830 | // Find all paths that exist in the DB but not in the filesystem | |
| 831 | const deletedPaths = Array.from(this.previousPaths).filter( | |
| 832 | (path) => !this.visitedPaths.has(path), | |
| 833 | ); | |
| 834 | ||
| 835 | for (const dbPath of deletedPaths) { | |
| 836 | const file = MediaFile.getByPath(dbPath); | |
| 837 | if (!file) continue; | |
| 838 | ||
| 839 | log(`Item Deleted: ${dbPath}`, true); | |
| 840 | if (file.processed === 2) { | |
| 841 | if (BlobAsset.decrementOrDelete(file.hash)) { | |
| 842 | log(`Deleted compressed asset ${file.hash}.{gzip, zstd}`, true); | |
| 843 | await fsp.unlink( | |
| 844 | path.join( | |
| 845 | COMPRESS_STORE, | |
| 846 | file.hash.substring(0, 2), | |
| 847 | file.hash + ".gz", | |
| 848 | ), | |
| 849 | ); | |
| 850 | await fsp.unlink( | |
| 851 | path.join( | |
| 852 | COMPRESS_STORE, | |
| 853 | file.hash.substring(0, 2), | |
| 854 | file.hash + ".zstd", | |
| 855 | ), | |
| 856 | ); | |
| 857 | } | |
| 858 | } | |
| 859 | MediaFile.deleteByPath(dbPath); | |
| 860 | } | |
| 861 | } | |
| 862 | ||
| 863 | async loadPreviousPaths(): Promise<void> { | |
| 864 | // Get all files and directories from the database | |
| 865 | // This uses a custom query to get all paths at once | |
| 866 | const getAllPathsQuery = cache | |
| 867 | .prepare(`SELECT path, kind FROM media_files`) | |
| 868 | .all() as { | |
| 869 | path: string; | |
| 870 | kind: MediaFile.Kind; | |
| 871 | }[]; | |
| 872 | ||
| 873 | let dirs = 0; | |
| 874 | let files = 0; | |
| 875 | for (const row of getAllPathsQuery) { | |
| 876 | this.previousPaths.add(row.path); | |
| 877 | if (row.kind === MediaFile.Kind.directory) { | |
| 878 | dirs++; | |
| 879 | } else { | |
| 880 | files++; | |
| 881 | } | |
| 882 | } | |
| 883 | ||
| 884 | this.dirQueue.setEstimate(dirs); | |
| 885 | this.fileQueue.setEstimate(files); | |
| 886 | ||
| 887 | // log(`Loaded ${this.previousPaths.size} paths from database`, true); | |
| 888 | } | |
| 889 | ||
| 890 | async scan(): Promise<void> { | |
| 891 | log(`Starting file system scan in ${LOCAL_DIR}`, true); | |
| 892 | ||
| 893 | // Check if the root directory exists and is accessible | |
| 894 | try { | |
| 895 | const rootStat = await fsp.stat(LOCAL_DIR); | |
| 896 | if (!rootStat.isDirectory()) { | |
| 897 | throw new Error(`${LOCAL_DIR} is not a directory`); | |
| 898 | } | |
| 899 | } catch (error) { | |
| 900 | console.error(`Error: Cannot access root directory ${LOCAL_DIR}`, error); | |
| 901 | console.error( | |
| 902 | `Aborting scan to prevent database corruption. Please check if the volume is mounted.`, | |
| 903 | ); | |
| 904 | process.exit(1); | |
| 905 | } | |
| 906 | ||
| 907 | await this.loadPreviousPaths(); | |
| 908 | ||
| 909 | await this.dirQueue.add(LOCAL_DIR, this.scanDirectory.bind(this)); | |
| 910 | ||
| 911 | await this.dirQueue.waitForCompletion(); | |
| 912 | await this.fileQueue.waitForCompletion(); | |
| 913 | ||
| 914 | await this.findDeletedFiles(); | |
| 915 | ||
| 916 | const allDirs = Array.from(this.visitedPaths) | |
| 917 | .filter((path) => { | |
| 918 | const file = MediaFile.getByPath(path); | |
| 919 | return file && file.kind === MediaFile.Kind.directory; | |
| 920 | }) | |
| 921 | .sort((a, b) => b.length - a.length); | |
| 922 | ||
| 923 | const dirMetadataQueue = new AsyncQueue<string>("Directory Metadata", 10); | |
| 924 | for (const dirPath of allDirs) { | |
| 925 | await this.processDirectoryMetadata(this.getLocalPath(dirPath)); | |
| 926 | } | |
| 927 | ||
| 928 | await dirMetadataQueue.waitForCompletion(); | |
| 929 | ||
| 930 | if (SHOULD_COMPRESS) { | |
| 931 | await this.compressQueue!.waitForCompletion(); | |
| 932 | } | |
| 933 | ||
| 934 | log("Scan completed successfully!", true); | |
| 935 | } | |
| 936 | } | |
| 937 | ||
| 938 | // Main execution | |
| 939 | function showHelp() { | |
| 940 | console.log(` | |
| 941 | MediaFile Scanner - Index filesystem content for paperclover.net | |
| 942 | ||
| 943 | Environment variables: | |
| 944 | FILE_ROOT Required. Path to the directory to scan | |
| 945 | COMPRESS_STORE Optional. Path to store compressed files (default: .clover/compressed) | |
| 946 | ||
| 947 | Options: | |
| 948 | --help Show this help message | |
| 949 | --dry-run Don't make any changes to the database | |
| 950 | --verbose Show detailed output | |
| 951 | ||
| 952 | Usage: | |
| 953 | bun ./media/scan.ts [options] | |
| 954 | ||
| 955 | `); | |
| 956 | process.exit(0); | |
| 957 | } | |
| 958 | ||
| 959 | { | |
| 960 | // Show help if requested | |
| 961 | if (process.argv.includes("--help")) { | |
| 962 | showHelp(); | |
| 963 | process.exit(0); | |
| 964 | } | |
| 965 | ||
| 966 | // Check if the root directory exists before starting | |
| 967 | if (!existsSync(LOCAL_DIR)) { | |
| 968 | console.error( | |
| 969 | `Error: Root directory ${LOCAL_DIR} does not exist or is not accessible.`, | |
| 970 | ); | |
| 971 | console.error(`Please check if the volume is mounted correctly.`); | |
| 972 | process.exit(1); | |
| 973 | } | |
| 974 | ||
| 975 | const startTime = Date.now(); | |
| 976 | ||
| 977 | try { | |
| 978 | const scanner = new FileSystemScanner(); | |
| 979 | await scanner.scan(); | |
| 980 | ||
| 981 | const endTime = Date.now(); | |
| 982 | log(`Scan completed in ${(endTime - startTime) / 1000} seconds`, true); | |
| 983 | ||
| 984 | const rootDir = MediaFile.getByPath("/")!; | |
| 985 | const totalEntries = cache | |
| 986 | .prepare(`SELECT COUNT(*) as count FROM media_files`) | |
| 987 | .get() as { count: number }; | |
| 988 | const totalDuration = cache | |
| 989 | .prepare(`SELECT SUM(duration) as duration FROM media_files`) | |
| 990 | .get() as { duration: number }; | |
| 991 | console.log(); | |
| 992 | console.log("Global Stats"); | |
| 993 | console.log(` Entry count: ${totalEntries.count}`); | |
| 994 | console.log(` Uncompressed size: ${formatSize(rootDir.size)}`); | |
| 995 | console.log( | |
| 996 | ` Total audio/video duration: ${ | |
| 997 | ( | |
| 998 | totalDuration.duration / | |
| 999 | 60 / | |
| 1000 | 60 | |
| 1001 | ).toFixed(1) | |
| 1002 | } hours`, | |
| 1003 | ); | |
| 1004 | } catch (error) { | |
| 1005 | console.error("Error during scan:", error); | |
| 1006 | process.exit(1); | |
| 1007 | } | |
| 1008 | } |
src/file-viewer/scripts/canvas_2017.client.ts created+233| ... | ... | @@ -0,0 +1,233 @@ |
| 1 | // Vibe coded with AI | |
| 2 | (globalThis as any).canvas_2017 = function (canvas: HTMLCanvasElement) { | |
| 3 | const isStandalone = canvas.getAttribute("data-standalone") === "true"; | |
| 4 | // Configuration interface for the checkerboard effect | |
| 5 | interface CheckerboardConfig { | |
| 6 | fps: number; // frames per second | |
| 7 | color1: string; // first checkerboard color | |
| 8 | color2: string; // second checkerboard color | |
| 9 | opacity: number; // opacity of each checkerboard (0-1) | |
| 10 | speedX1: number; // horizontal speed of first checkerboard (pixels per second) | |
| 11 | speedY1: number; // vertical speed of first checkerboard (pixels per second) | |
| 12 | speedX2: number; // horizontal speed of second checkerboard (pixels per second) | |
| 13 | speedY2: number; // vertical speed of second checkerboard (pixels per second) | |
| 14 | baseTileSize: number; // base size of checkerboard tiles | |
| 15 | sizeVariation: number; // maximum variation in tile size (pixels) | |
| 16 | sineFrequency1: number; // frequency of first sine wave for size variation | |
| 17 | sineFrequency2: number; // frequency of second sine wave for size variation | |
| 18 | sineOffset: number; // offset between the two sine waves (radians) | |
| 19 | rotation: number; // rotation in degrees for the entire pattern | |
| 20 | rotation2: number; // rotation in degrees for the entire pattern | |
| 21 | } | |
| 22 | ||
| 23 | // Default configuration | |
| 24 | const config: CheckerboardConfig = { | |
| 25 | fps: 30, | |
| 26 | color1: "#1A1C17", | |
| 27 | color2: "#1A1C17", | |
| 28 | opacity: 0.3, | |
| 29 | speedX1: -0.02, // moving left slowly | |
| 30 | speedY1: -0.01, // moving up slowly | |
| 31 | speedX2: -0.015, // moving left (slightly slower) | |
| 32 | speedY2: 0.012, // moving down slowly | |
| 33 | baseTileSize: 200, | |
| 34 | sizeVariation: 1.5, | |
| 35 | sineFrequency1: 0.0005, | |
| 36 | sineFrequency2: 0.0008, | |
| 37 | sineOffset: Math.PI / 2, // 90 degrees offset | |
| 38 | rotation: 2, // 5 degree rotation | |
| 39 | rotation2: -2, // 5 degree rotation | |
| 40 | }; | |
| 41 | ||
| 42 | // Get the canvas context | |
| 43 | const ctx = canvas.getContext("2d"); | |
| 44 | if (!ctx) { | |
| 45 | console.error("Could not get canvas context"); | |
| 46 | return () => {}; | |
| 47 | } | |
| 48 | ||
| 49 | // Make canvas transparent | |
| 50 | if (isStandalone) { | |
| 51 | canvas.style.backgroundColor = "#737D60"; | |
| 52 | } else { | |
| 53 | canvas.style.backgroundColor = "transparent"; | |
| 54 | } | |
| 55 | ||
| 56 | // Variables to track position and animation | |
| 57 | let width = canvas.width; | |
| 58 | let height = canvas.height; | |
| 59 | let animationFrameId: number; | |
| 60 | let lastFrameTime = 0; | |
| 61 | const frameInterval = 1000 / config.fps; | |
| 62 | ||
| 63 | // Position offsets for the two checkerboards (centered) | |
| 64 | let offset1X = 0; | |
| 65 | let offset1Y = 0; | |
| 66 | let offset2X = 0; | |
| 67 | let offset2Y = 0; | |
| 68 | ||
| 69 | // Time variable for sine wave calculation | |
| 70 | let time = 0; | |
| 71 | ||
| 72 | // Convert rotation to radians | |
| 73 | const rotationRad = (config.rotation * Math.PI) / 180; | |
| 74 | const rotationRad2 = (config.rotation2 * Math.PI) / 180; | |
| 75 | ||
| 76 | // Update canvas dimensions when resized | |
| 77 | const updateDimensions = () => { | |
| 78 | width = canvas.width = canvas.clientWidth; | |
| 79 | height = canvas.height = canvas.clientHeight; | |
| 80 | }; | |
| 81 | ||
| 82 | // Calculate the diagonal length of the canvas (to ensure rotation covers corners) | |
| 83 | const calculateDiagonal = () => { | |
| 84 | return Math.sqrt(width * width + height * height); | |
| 85 | }; | |
| 86 | ||
| 87 | // Draw a single checkerboard pattern scaled from center with rotation | |
| 88 | const drawCheckerboard = ( | |
| 89 | offsetX: number, | |
| 90 | offsetY: number, | |
| 91 | tileSize: number, | |
| 92 | color1: string, | |
| 93 | color2: string, | |
| 94 | opacity: number, | |
| 95 | rotationRad: number, | |
| 96 | ) => { | |
| 97 | ctx.globalAlpha = opacity; | |
| 98 | ||
| 99 | // Get the center of the viewport | |
| 100 | const centerX = width / 2; | |
| 101 | const centerY = height / 2; | |
| 102 | ||
| 103 | // Save the current transformation state | |
| 104 | ctx.save(); | |
| 105 | ||
| 106 | // Move to the center of the canvas, rotate, then move back | |
| 107 | ctx.translate(centerX, centerY); | |
| 108 | ctx.rotate(rotationRad); | |
| 109 | ||
| 110 | // Calculate the number of tiles needed to cover the rotated canvas | |
| 111 | // We need to use the diagonal length to ensure we cover the corners when rotated | |
| 112 | const diagonal = calculateDiagonal(); | |
| 113 | const tilesX = Math.ceil(diagonal / tileSize) + 6; // Added extra tiles for rotation | |
| 114 | const tilesY = Math.ceil(diagonal / tileSize) + 6; | |
| 115 | ||
| 116 | // Calculate how many tiles fit from center to edge (in each direction) | |
| 117 | const halfTilesX = Math.ceil(tilesX / 2); | |
| 118 | const halfTilesY = Math.ceil(tilesY / 2); | |
| 119 | ||
| 120 | // Adjust the offset to be relative to the center | |
| 121 | // The modulo ensures the pattern repeats smoothly even with scaling | |
| 122 | const adjustedOffsetX = offsetX % (tileSize * 2); | |
| 123 | const adjustedOffsetY = offsetY % (tileSize * 2); | |
| 124 | ||
| 125 | // Draw the checker pattern, centered on the viewport | |
| 126 | for (let y = -halfTilesY; y <= halfTilesY; y++) { | |
| 127 | for (let x = -halfTilesX; x <= halfTilesX; x++) { | |
| 128 | // Determine if this tile should be colored (creating checker pattern) | |
| 129 | // We add a large number to ensure (x+y) is always positive for the modulo | |
| 130 | if ((x + y + 1000) % 2 === 0) { | |
| 131 | ctx.fillStyle = color1; | |
| 132 | } else { | |
| 133 | ctx.fillStyle = color2; | |
| 134 | } | |
| 135 | ||
| 136 | // Calculate the position of this tile relative to the center | |
| 137 | // The adjusted offset creates the movement effect | |
| 138 | const posX = (x * tileSize) + adjustedOffsetX; | |
| 139 | const posY = (y * tileSize) + adjustedOffsetY; | |
| 140 | ||
| 141 | // Draw the tile | |
| 142 | ctx.fillRect( | |
| 143 | posX - tileSize / 2, | |
| 144 | posY - tileSize / 2, | |
| 145 | tileSize, | |
| 146 | tileSize, | |
| 147 | ); | |
| 148 | } | |
| 149 | } | |
| 150 | ||
| 151 | // Restore the transformation state | |
| 152 | ctx.restore(); | |
| 153 | }; | |
| 154 | ||
| 155 | // Animation loop | |
| 156 | const animate = (currentTime: number) => { | |
| 157 | animationFrameId = requestAnimationFrame(animate); | |
| 158 | ||
| 159 | // Control frame rate | |
| 160 | if (currentTime - lastFrameTime < frameInterval) { | |
| 161 | return; | |
| 162 | } | |
| 163 | ||
| 164 | // Calculate the time elapsed since the last frame | |
| 165 | const dt = currentTime - lastFrameTime; | |
| 166 | lastFrameTime = currentTime; | |
| 167 | ||
| 168 | // Increment time for sine wave calculation | |
| 169 | time += dt; | |
| 170 | ||
| 171 | // Update the position offsets based on speed and elapsed time | |
| 172 | offset1X += config.speedX1 * dt; | |
| 173 | offset1Y += config.speedY1 * dt; | |
| 174 | offset2X += config.speedX2 * dt; | |
| 175 | offset2Y += config.speedY2 * dt; | |
| 176 | ||
| 177 | // Calculate the tile sizes using sine waves | |
| 178 | const tileSize1 = config.baseTileSize + | |
| 179 | Math.sin(time * config.sineFrequency1) * config.sizeVariation; | |
| 180 | const tileSize2 = config.baseTileSize + | |
| 181 | Math.sin(time * config.sineFrequency2 + config.sineOffset) * | |
| 182 | config.sizeVariation; | |
| 183 | ||
| 184 | // Clear canvas | |
| 185 | ctx.clearRect(0, 0, width, height); | |
| 186 | ||
| 187 | // Draw the two checkerboards | |
| 188 | drawCheckerboard( | |
| 189 | offset1X, | |
| 190 | offset1Y, | |
| 191 | tileSize1, | |
| 192 | config.color1, | |
| 193 | "transparent", | |
| 194 | config.opacity, | |
| 195 | rotationRad, | |
| 196 | ); | |
| 197 | ||
| 198 | drawCheckerboard( | |
| 199 | offset2X, | |
| 200 | offset2Y, | |
| 201 | tileSize2, | |
| 202 | config.color2, | |
| 203 | "transparent", | |
| 204 | config.opacity, | |
| 205 | rotationRad2, | |
| 206 | ); | |
| 207 | ||
| 208 | // Reset global alpha | |
| 209 | ctx.globalAlpha = 1.0; | |
| 210 | }; | |
| 211 | ||
| 212 | // Initialize the animation | |
| 213 | const init = () => { | |
| 214 | // Set up resize handler | |
| 215 | globalThis.addEventListener("resize", updateDimensions); | |
| 216 | ||
| 217 | // Initial setup | |
| 218 | updateDimensions(); | |
| 219 | ||
| 220 | // Start animation | |
| 221 | lastFrameTime = performance.now(); | |
| 222 | animationFrameId = requestAnimationFrame(animate); | |
| 223 | }; | |
| 224 | ||
| 225 | // Start the animation | |
| 226 | init(); | |
| 227 | ||
| 228 | // Return cleanup function | |
| 229 | return () => { | |
| 230 | globalThis.removeEventListener("resize", updateDimensions); | |
| 231 | cancelAnimationFrame(animationFrameId); | |
| 232 | }; | |
| 233 | }; |
src/file-viewer/scripts/canvas_2018.client.ts created+431| ... | ... | @@ -0,0 +1,431 @@ |
| 1 | // This canvas is based on the maze generation algo in Tanks. This was | |
| 2 | // originally written in C++ as a single function in 2018, and was ported to TS | |
| 3 | // by Chloe in 2025 for the cotyledon canvas. | |
| 4 | // | |
| 5 | // The main difference is that this version is a visualization, rather than the | |
| 6 | // practical function. Instead of taking a millisecond, only 5 steps are | |
| 7 | // performed per second, visualizing the whole ordeal. It also isn't a playable | |
| 8 | // game, obviously. | |
| 9 | // | |
| 10 | // Ported with love because I care about my old self | |
| 11 | // She deserves the world, but instead gave it to me. | |
| 12 | (globalThis as any).canvas_2018 = function (canvas: HTMLCanvasElement) { | |
| 13 | const isStandalone = canvas.getAttribute("data-standalone") === "true"; | |
| 14 | if (isStandalone) { | |
| 15 | canvas.style.backgroundColor = "#27201E"; | |
| 16 | } | |
| 17 | interface Cell { | |
| 18 | down: boolean; | |
| 19 | right: boolean; | |
| 20 | visited: boolean; | |
| 21 | ||
| 22 | cell_flash: number; | |
| 23 | down_flash: number; | |
| 24 | right_flash: number; | |
| 25 | } | |
| 26 | interface Pos { | |
| 27 | x: number; | |
| 28 | y: number; | |
| 29 | /** Where the wall is relative to x, y. */ | |
| 30 | dir: "left" | "right" | "up" | "down"; | |
| 31 | } | |
| 32 | interface Maze { | |
| 33 | grid: Grid; | |
| 34 | cursor: { x: number; y: number }; | |
| 35 | lastTick: number; | |
| 36 | /* Pixels */ | |
| 37 | transform: number; | |
| 38 | newCellsToVisit: Pos[]; | |
| 39 | randomWallBag: Cell[]; | |
| 40 | randomWallTarget: number; | |
| 41 | renderOffset: { x: number; y: number }; | |
| 42 | done: boolean; | |
| 43 | } | |
| 44 | const hex = (color: number[]) => | |
| 45 | "#" + color.map((c) => c.toString(16).padStart(2, "0")).join(""); | |
| 46 | let cellSize: number; | |
| 47 | let borderThickness: number; | |
| 48 | const cellFlashModifier = isStandalone ? 0.4 : 0.2; | |
| 49 | const color = isStandalone ? "#170d0b" : "#231C1A"; | |
| 50 | const bg = [0x27, 0x20, 0x1E]; | |
| 51 | const wallFlashColor = [0xFF, 0xA8, 0x7A]; | |
| 52 | const cellFlashColor = "#FFA87A"; | |
| 53 | const updateTime = 1000 / 7; | |
| 54 | const randomWallBreakInterval = [6, 12]; // every 10 to 18 walls. | |
| 55 | function randomBetween(min: number, max: number) { | |
| 56 | return Math.round( | |
| 57 | Math.random() * (max - min), | |
| 58 | ) + min; | |
| 59 | } | |
| 60 | function randomOf<T>(array: T[]): T { | |
| 61 | return array[randomBetween(0, array.length - 1)]; | |
| 62 | } | |
| 63 | function randomWallTarget() { | |
| 64 | return randomBetween( | |
| 65 | randomWallBreakInterval[0], | |
| 66 | randomWallBreakInterval[1], | |
| 67 | ); | |
| 68 | } | |
| 69 | ||
| 70 | // Originally, this used a 2-dimensional array. However, I wanted to make sure | |
| 71 | // that the grid could be infinitely sized. This grid constructs new cells on | |
| 72 | // demand, as needed. | |
| 73 | class Grid { | |
| 74 | cells = new Map<number, Cell>(); | |
| 75 | cell({ x, y }: { x: number; y: number }) { | |
| 76 | const k = ((x | 0) << 16) + (y | 0); | |
| 77 | const { cells } = this; | |
| 78 | let existing = this.cells.get(k); | |
| 79 | if (!existing) { | |
| 80 | existing = { | |
| 81 | cell_flash: 0, | |
| 82 | down: true, | |
| 83 | down_flash: 0, | |
| 84 | right: true, | |
| 85 | right_flash: 0, | |
| 86 | visited: false, | |
| 87 | }; | |
| 88 | cells.set(k, existing); | |
| 89 | } | |
| 90 | return existing; | |
| 91 | } | |
| 92 | forAll( | |
| 93 | renderOffset: { x: number; y: number }, | |
| 94 | width: number, | |
| 95 | height: number, | |
| 96 | cb: (cell: Cell, pos: { x: number; y: number }) => void, | |
| 97 | ) { | |
| 98 | const { x: offsetX, y: offsetY } = renderOffset; | |
| 99 | const startX = Math.floor(-offsetX / cellSize); | |
| 100 | const startY = Math.floor(-offsetY / cellSize); | |
| 101 | const endX = Math.ceil((width - offsetX) / cellSize); | |
| 102 | const endY = Math.ceil((height - offsetY) / cellSize); | |
| 103 | for (let x = startX; x <= endX; x++) { | |
| 104 | for (let y = startY; y <= endY; y++) { | |
| 105 | const cellX = offsetX + x * cellSize; | |
| 106 | const cellY = offsetY + y * cellSize; | |
| 107 | cb(this.cell({ x, y }), { x: cellX, y: cellY }); | |
| 108 | } | |
| 109 | } | |
| 110 | } | |
| 111 | } | |
| 112 | ||
| 113 | const ctx = canvas.getContext("2d")!; | |
| 114 | if (!ctx) { | |
| 115 | console.error("Could not get canvas context"); | |
| 116 | return () => {}; | |
| 117 | } | |
| 118 | ||
| 119 | let width: number, height: number; | |
| 120 | const updateDimensions = () => { | |
| 121 | width = canvas.width = canvas.offsetWidth; | |
| 122 | height = canvas.height = canvas.offsetHeight; | |
| 123 | cellSize = 100; | |
| 124 | borderThickness = 8; | |
| 125 | }; | |
| 126 | updateDimensions(); | |
| 127 | ||
| 128 | setTimeout(() => { | |
| 129 | updateDimensions(); | |
| 130 | }, 10); | |
| 131 | ||
| 132 | let maze = initMaze(); | |
| 133 | let nextMaze: Maze | null = null; | |
| 134 | let completeFade = 0; | |
| 135 | function initMaze(): Maze { | |
| 136 | return { | |
| 137 | grid: new Grid(), | |
| 138 | transform: 0, | |
| 139 | cursor: { | |
| 140 | x: randomBetween(0, Math.ceil(width / cellSize)), | |
| 141 | y: randomBetween(0, Math.ceil(height / cellSize)), | |
| 142 | }, | |
| 143 | lastTick: performance.now(), | |
| 144 | randomWallBag: [], | |
| 145 | randomWallTarget: randomWallTarget(), | |
| 146 | newCellsToVisit: [], | |
| 147 | renderOffset: { x: 0, y: 0 }, | |
| 148 | done: false, | |
| 149 | }; | |
| 150 | } | |
| 151 | ||
| 152 | function isOnScreen(maze: Maze, x: number, y: number) { | |
| 153 | const { x: offsetX, y: offsetY } = maze.renderOffset; | |
| 154 | const cellX = offsetX + x * cellSize; | |
| 155 | const cellY = offsetY + y * cellSize; | |
| 156 | return ( | |
| 157 | cellX + cellSize > 0 && | |
| 158 | cellX < width && | |
| 159 | cellY + cellSize > 0 && | |
| 160 | cellY < height | |
| 161 | ); | |
| 162 | } | |
| 163 | ||
| 164 | function tick(maze: Maze, other?: Maze) { | |
| 165 | if (maze.done) return; | |
| 166 | ||
| 167 | // The original maze algorithm broke down 4%-8% of random right facing | |
| 168 | // walls, and 4%-8% of down facing walls. It did this at the end. | |
| 169 | // To make this visual more interesting, two random walls will be broken | |
| 170 | // down every 12-25 cell visits. This way, the main trail is always running. | |
| 171 | if (maze.randomWallBag.length > maze.randomWallTarget) { | |
| 172 | const down: Cell = randomOf(maze.randomWallBag); | |
| 173 | const right: Cell = randomOf(maze.randomWallBag); | |
| 174 | maze.randomWallBag.forEach((cell) => | |
| 175 | cell.cell_flash = Math.min(cell.cell_flash + 0.2, 1) | |
| 176 | ); | |
| 177 | down.cell_flash = 1; | |
| 178 | down.down = false; | |
| 179 | down.down_flash = 1; | |
| 180 | right.cell_flash = 1; | |
| 181 | right.right = false; | |
| 182 | right.right_flash = 1; | |
| 183 | maze.randomWallBag = []; | |
| 184 | maze.randomWallTarget = randomWallTarget(); | |
| 185 | return; | |
| 186 | } | |
| 187 | ||
| 188 | // The main algorithm was simple: Have a cursor position, and move it in a | |
| 189 | // random direction that it had not seen before. Once it had run out of | |
| 190 | // options, branch off of a previous location. Only visit each cell once. | |
| 191 | // | |
| 192 | // In this visualization, cells that are too far offscreen are softly | |
| 193 | // treated as "visited", which is how the simulation always stays in frame. | |
| 194 | const current = maze.grid.cell(maze.cursor); | |
| 195 | current.visited = true; | |
| 196 | current.cell_flash = 1; | |
| 197 | maze.randomWallBag.push(current); | |
| 198 | const adjacent = ([ | |
| 199 | { x: maze.cursor.x + 1, y: maze.cursor.y, dir: "left" }, | |
| 200 | { x: maze.cursor.x - 1, y: maze.cursor.y, dir: "right" }, | |
| 201 | { x: maze.cursor.x, y: maze.cursor.y + 1, dir: "up" }, | |
| 202 | { x: maze.cursor.x, y: maze.cursor.y - 1, dir: "down" }, | |
| 203 | ] as Pos[]).filter((pos) => | |
| 204 | isOnScreen(maze, pos.x, pos.y) && | |
| 205 | maze.grid.cell(pos).visited === false | |
| 206 | ); | |
| 207 | if (adjacent.length === 0) { | |
| 208 | // move cursor to a random cell that has not been visited. | |
| 209 | const cells = maze.newCellsToVisit.filter((pos) => | |
| 210 | isOnScreen(maze, pos.x, pos.y) && | |
| 211 | maze.grid.cell(pos).visited === false | |
| 212 | ); | |
| 213 | if (cells.length === 0) { | |
| 214 | maze.done = true; | |
| 215 | return; | |
| 216 | } | |
| 217 | const continuePos = randomOf(cells); | |
| 218 | breakWall(maze, continuePos, other); | |
| 219 | maze.cursor = { x: continuePos.x, y: continuePos.y }; | |
| 220 | return; | |
| 221 | } | |
| 222 | ||
| 223 | // break a random wall | |
| 224 | const toBreak = randomOf(adjacent); | |
| 225 | breakWall(maze, toBreak, other); | |
| 226 | maze.cursor = { x: toBreak.x, y: toBreak.y }; | |
| 227 | ||
| 228 | // add the other directions to the new cells to visit. | |
| 229 | maze.newCellsToVisit.push( | |
| 230 | ...adjacent.filter((pos) => pos.dir !== toBreak.dir), | |
| 231 | ); | |
| 232 | } | |
| 233 | ||
| 234 | function breakWall(maze: Maze, pos: Pos, other?: Maze) { | |
| 235 | if (pos.dir === "right") { | |
| 236 | const cell = maze.grid.cell(pos); | |
| 237 | cell.right = false; | |
| 238 | cell.right_flash = 1; | |
| 239 | if (other) cell.right = false; | |
| 240 | } else if (pos.dir === "down") { | |
| 241 | const cell = maze.grid.cell(pos); | |
| 242 | cell.down = false; | |
| 243 | cell.down_flash = 1; | |
| 244 | if (other) cell.down = false; | |
| 245 | } else if (pos.dir === "left") { | |
| 246 | const cell = maze.grid.cell({ x: pos.x - 1, y: pos.y }); | |
| 247 | cell.right = false; | |
| 248 | cell.right_flash = 1; | |
| 249 | if (other) cell.right = false; | |
| 250 | } else if (pos.dir === "up") { | |
| 251 | const cell = maze.grid.cell({ x: pos.x, y: pos.y - 1 }); | |
| 252 | cell.down = false; | |
| 253 | cell.down_flash = 1; | |
| 254 | if (other) cell.down = false; | |
| 255 | } | |
| 256 | } | |
| 257 | ||
| 258 | function renderOffset(maze: Maze) { | |
| 259 | return { x: maze.transform, y: maze.transform }; | |
| 260 | } | |
| 261 | ||
| 262 | let animationFrameId: number; | |
| 263 | let last = performance.now(); | |
| 264 | let dt: number = 0; | |
| 265 | ||
| 266 | function renderMazeBorders(maze: Maze, opacity: number) { | |
| 267 | ctx.globalAlpha = opacity; | |
| 268 | maze.grid.forAll( | |
| 269 | maze.renderOffset, | |
| 270 | width, | |
| 271 | height, | |
| 272 | (cell, { x: cellX, y: cellY }) => { | |
| 273 | // Walls | |
| 274 | if (cell.right) { | |
| 275 | ctx.fillStyle = color; | |
| 276 | ctx.fillRect( | |
| 277 | cellX + cellSize - borderThickness / 2, | |
| 278 | cellY - borderThickness / 2, | |
| 279 | borderThickness, | |
| 280 | cellSize + borderThickness, | |
| 281 | ); | |
| 282 | } | |
| 283 | if (cell.down) { | |
| 284 | ctx.fillStyle = color; | |
| 285 | ctx.fillRect( | |
| 286 | cellX - borderThickness / 2, | |
| 287 | cellY + cellSize - borderThickness / 2, | |
| 288 | cellSize + borderThickness, | |
| 289 | borderThickness, | |
| 290 | ); | |
| 291 | } | |
| 292 | }, | |
| 293 | ); | |
| 294 | ctx.globalAlpha = 1; | |
| 295 | } | |
| 296 | ||
| 297 | function renderCellFlash(maze: Maze) { | |
| 298 | maze.grid.forAll( | |
| 299 | maze.renderOffset, | |
| 300 | width, | |
| 301 | height, | |
| 302 | (cell, { x: cellX, y: cellY }) => { | |
| 303 | // Cell flash to show visiting path. | |
| 304 | if (cell.cell_flash > 0) { | |
| 305 | cell.cell_flash = Math.max(0, cell.cell_flash - dt / 1000); | |
| 306 | ctx.fillStyle = cellFlashColor; | |
| 307 | ctx.globalAlpha = cell.cell_flash * cellFlashModifier; | |
| 308 | ctx.fillRect(cellX, cellY, cellSize, cellSize); | |
| 309 | ctx.globalAlpha = 1; | |
| 310 | } | |
| 311 | }, | |
| 312 | ); | |
| 313 | } | |
| 314 | ||
| 315 | function renderBorderFlash(maze: Maze) { | |
| 316 | maze.grid.forAll( | |
| 317 | maze.renderOffset, | |
| 318 | width, | |
| 319 | height, | |
| 320 | (cell, { x: cellX, y: cellY }) => { | |
| 321 | if (cell.right_flash == 0 && cell.down_flash == 0) { | |
| 322 | return; | |
| 323 | } | |
| 324 | ||
| 325 | // Walls | |
| 326 | const cellFlash = cell.cell_flash * cellFlashModifier; | |
| 327 | if (cell.right_flash > 0) { | |
| 328 | cell.right_flash = Math.max(0, cell.right_flash - dt / 500); | |
| 329 | ctx.fillStyle = interpolateColor( | |
| 330 | bg, | |
| 331 | wallFlashColor, | |
| 332 | Math.max(cell.right_flash, cellFlash), | |
| 333 | ); | |
| 334 | if (cellFlash > cell.right_flash) { | |
| 335 | ctx.globalAlpha = cell.right_flash / cellFlash; | |
| 336 | } | |
| 337 | ctx.fillRect( | |
| 338 | cellX + cellSize - borderThickness / 2, | |
| 339 | cellY + borderThickness / 2, | |
| 340 | borderThickness, | |
| 341 | cellSize - borderThickness, | |
| 342 | ); | |
| 343 | ctx.globalAlpha = 1; | |
| 344 | } | |
| 345 | if (cell.down_flash > 0) { | |
| 346 | if (cellFlash > cell.down_flash) { | |
| 347 | ctx.globalAlpha = cell.down_flash / cellFlash; | |
| 348 | } | |
| 349 | cell.down_flash = Math.max(0, cell.down_flash - dt / 500); | |
| 350 | ctx.fillStyle = interpolateColor( | |
| 351 | bg, | |
| 352 | wallFlashColor, | |
| 353 | Math.max(cell.down_flash, cellFlash), | |
| 354 | ); | |
| 355 | ctx.fillRect( | |
| 356 | cellX + borderThickness / 2, | |
| 357 | cellY + cellSize - borderThickness / 2, | |
| 358 | cellSize - borderThickness, | |
| 359 | borderThickness, | |
| 360 | ); | |
| 361 | ctx.globalAlpha = 1; | |
| 362 | } | |
| 363 | }, | |
| 364 | ); | |
| 365 | } | |
| 366 | ||
| 367 | function render() { | |
| 368 | const now = performance.now(); | |
| 369 | dt = now - last; | |
| 370 | maze.transform += dt * 0.005; | |
| 371 | maze.renderOffset = renderOffset(maze); | |
| 372 | if (!maze.done) { | |
| 373 | if (now - maze.lastTick >= updateTime) { | |
| 374 | tick(maze); | |
| 375 | maze.lastTick = now; | |
| 376 | ||
| 377 | if (maze.done) { | |
| 378 | nextMaze = initMaze(); | |
| 379 | nextMaze.transform = (maze.transform % cellSize) - dt * 0.005; | |
| 380 | nextMaze.lastTick = now; | |
| 381 | completeFade = 0; | |
| 382 | } | |
| 383 | } | |
| 384 | } | |
| 385 | if (nextMaze) { | |
| 386 | nextMaze.transform += dt * 0.005; | |
| 387 | nextMaze.renderOffset = renderOffset(nextMaze); | |
| 388 | if (!nextMaze.done && now - nextMaze.lastTick >= updateTime) { | |
| 389 | tick(nextMaze, maze); | |
| 390 | nextMaze.lastTick = now; | |
| 391 | } | |
| 392 | } | |
| 393 | last = now; | |
| 394 | ||
| 395 | ctx.clearRect(0, 0, width, height); | |
| 396 | ||
| 397 | renderCellFlash(maze); | |
| 398 | if (nextMaze) renderCellFlash(nextMaze); | |
| 399 | ||
| 400 | renderMazeBorders(maze, 1); | |
| 401 | if (nextMaze) { | |
| 402 | renderMazeBorders(nextMaze, completeFade); | |
| 403 | completeFade += dt / 3000; | |
| 404 | if (completeFade >= 1) { | |
| 405 | maze = nextMaze; | |
| 406 | nextMaze = null; | |
| 407 | } | |
| 408 | } | |
| 409 | ||
| 410 | renderBorderFlash(maze); | |
| 411 | if (nextMaze) { | |
| 412 | renderCellFlash(nextMaze); | |
| 413 | renderBorderFlash(nextMaze); | |
| 414 | } | |
| 415 | ||
| 416 | animationFrameId = requestAnimationFrame(render); | |
| 417 | } | |
| 418 | ||
| 419 | function interpolateColor(start: number[], end: number[], t: number) { | |
| 420 | return hex(start.map((s, i) => Math.round(s + (end[i] - s) * t))); | |
| 421 | } | |
| 422 | ||
| 423 | globalThis.addEventListener("resize", updateDimensions); | |
| 424 | animationFrameId = requestAnimationFrame(render); | |
| 425 | ||
| 426 | // cleanup function | |
| 427 | return () => { | |
| 428 | globalThis.removeEventListener("resize", updateDimensions); | |
| 429 | cancelAnimationFrame(animationFrameId); | |
| 430 | }; | |
| 431 | }; |
src/file-viewer/scripts/canvas_2019.client.ts created+109| ... | ... | @@ -0,0 +1,109 @@ |
| 1 | // __esModule | |
| 2 | true | |
| 3 | ||
| 4 | // assertAllowedAttributes | |
| 5 | function assertAllowedAttributes(path, allowed) { | |
| 6 | let i = 0; | |
| 7 | for (const attr of path.node.attributes) { | |
| 8 | if (attr.type === "MarkoSpreadAttribute") { | |
| 9 | throw path.hub.buildError( | |
| 10 | attr, | |
| 11 | `Tag does not support spread attributes.` | |
| 12 | ); | |
| 13 | } else if (!allowed.includes(attr.name)) { | |
| 14 | throw path.hub.buildError( | |
| 15 | attr, | |
| 16 | `Tag does not support the \`${attr.name}\` attribute.` | |
| 17 | ); | |
| 18 | } | |
| 19 | ||
| 20 | i++; | |
| 21 | } | |
| 22 | } | |
| 23 | ||
| 24 | // assertAttributesOrArgs | |
| 25 | function assertAttributesOrArgs(path) { | |
| 26 | const { node } = path; | |
| 27 | const args = node.arguments; | |
| 28 | if (args && args.length && (node.attributes.length > 0 || node.body.length)) { | |
| 29 | const start = args[0].loc.start; | |
| 30 | const end = args[args.length - 1].loc.end; | |
| 31 | throw path.hub.buildError( | |
| 32 | { loc: { start, end } }, | |
| 33 | "Tag does not support arguments when attributes or body present." | |
| 34 | ); | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | // assertAttributesOrSingleArg | |
| 39 | function assertAttributesOrSingleArg(path) { | |
| 40 | assertAttributesOrArgs(path); | |
| 41 | const args = path.node.arguments; | |
| 42 | if (args && args.length > 1) { | |
| 43 | const start = args[1].loc.start; | |
| 44 | const end = args[args.length - 1].loc.end; | |
| 45 | throw path.hub.buildError( | |
| 46 | { loc: { start, end } }, | |
| 47 | "Tag does not support multiple arguments." | |
| 48 | ); | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | // assertNoArgs | |
| 53 | function assertNoArgs(path) { | |
| 54 | const args = path.node.arguments; | |
| 55 | if (args && args.length) { | |
| 56 | const start = args[0].loc.start; | |
| 57 | const end = args[args.length - 1].loc.end; | |
| 58 | throw path.hub.buildError( | |
| 59 | { loc: { start, end } }, | |
| 60 | "Tag does not support arguments." | |
| 61 | ); | |
| 62 | } | |
| 63 | } | |
| 64 | ||
| 65 | // assertNoAttributeTags | |
| 66 | function assertNoAttributeTags(path) { | |
| 67 | if (path.node.attributeTags.length) { | |
| 68 | throw path.hub.buildError( | |
| 69 | path.node.attributeTags[0], | |
| 70 | "Tag not support nested attribute tags." | |
| 71 | ); | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | // assertNoAttributes | |
| 76 | function assertNoAttributes(path) { | |
| 77 | const { attributes } = path.node; | |
| 78 | if (attributes.length) { | |
| 79 | const start = attributes[0].loc.start; | |
| 80 | const end = attributes[attributes.length - 1].loc.end; | |
| 81 | throw path.hub.buildError( | |
| 82 | { loc: { start, end } }, | |
| 83 | "Tag does not support attributes." | |
| 84 | ); | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | // assertNoParams | |
| 89 | function assertNoParams(path) { | |
| 90 | const { params } = path.node.body; | |
| 91 | if (params.length) { | |
| 92 | const start = params[0].loc.start; | |
| 93 | const end = params[params.length - 1].loc.end; | |
| 94 | throw path.hub.buildError( | |
| 95 | { loc: { start, end } }, | |
| 96 | "Tag does not support parameters." | |
| 97 | ); | |
| 98 | } | |
| 99 | } | |
| 100 | ||
| 101 | // assertNoVar | |
| 102 | function assertNoVar(path) { | |
| 103 | if (path.node.var) { | |
| 104 | throw path.hub.buildError( | |
| 105 | path.node.var, | |
| 106 | "Tag does not support a variable." | |
| 107 | ); | |
| 108 | } | |
| 109 | } |
src/file-viewer/scripts/canvas_2020.client.ts created+197| ... | ... | @@ -0,0 +1,197 @@ |
| 1 | // Vibe coded with AI | |
| 2 | (globalThis as any).canvas_2020 = function (canvas: HTMLCanvasElement) { | |
| 3 | const isStandalone = canvas.getAttribute("data-standalone") === "true"; | |
| 4 | // Rain effect with slanted lines | |
| 5 | // Configuration interface for the rain effect | |
| 6 | interface RainConfig { | |
| 7 | fps: number; // frames per second | |
| 8 | color: string; // color of rain particles | |
| 9 | angle: number; // angle in degrees | |
| 10 | particleDensity: number; // particles per 10000 pixels of canvas area | |
| 11 | speed: number; // speed of particles (pixels per frame) | |
| 12 | lineWidth: number; // thickness of rain lines | |
| 13 | lineLength: number; // length of rain lines | |
| 14 | } | |
| 15 | ||
| 16 | // Rain particle interface | |
| 17 | interface RainParticle { | |
| 18 | x: number; // x position | |
| 19 | y: number; // y position | |
| 20 | } | |
| 21 | ||
| 22 | // Default configuration | |
| 23 | const config: RainConfig = { | |
| 24 | fps: 16, | |
| 25 | color: isStandalone ? "#00FEFB99" : "#081F24", | |
| 26 | angle: -18, | |
| 27 | particleDensity: 1, | |
| 28 | speed: 400, | |
| 29 | lineWidth: 8, | |
| 30 | lineLength: 100, | |
| 31 | }; | |
| 32 | ||
| 33 | // Get the canvas context | |
| 34 | const ctx = canvas.getContext("2d"); | |
| 35 | if (!ctx) { | |
| 36 | console.error("Could not get canvas context"); | |
| 37 | return () => {}; | |
| 38 | } | |
| 39 | ||
| 40 | // Make canvas transparent | |
| 41 | if (isStandalone) { | |
| 42 | canvas.style.backgroundColor = "#0F252B"; | |
| 43 | } else { | |
| 44 | canvas.style.backgroundColor = "transparent"; | |
| 45 | } | |
| 46 | ||
| 47 | // Calculate canvas dimensions and update when resized | |
| 48 | let width = canvas.width; | |
| 49 | let height = canvas.height; | |
| 50 | let particles: RainParticle[] = []; | |
| 51 | let animationFrameId: number; | |
| 52 | let lastFrameTime = 0; | |
| 53 | const frameInterval = 1000 / config.fps; | |
| 54 | ||
| 55 | // Calculate angle in radians | |
| 56 | const angleRad = (config.angle * Math.PI) / 180; | |
| 57 | ||
| 58 | // Update canvas dimensions and particle count when resized | |
| 59 | const updateDimensions = () => { | |
| 60 | width = canvas.width = canvas.offsetWidth; | |
| 61 | height = canvas.height = canvas.offsetHeight; | |
| 62 | ||
| 63 | // Calculate the canvas area in pixels | |
| 64 | const canvasArea = width * height; | |
| 65 | ||
| 66 | // Calculate target number of particles based on canvas area | |
| 67 | const targetParticleCount = Math.floor( | |
| 68 | (canvasArea / 10000) * config.particleDensity, | |
| 69 | ); | |
| 70 | ||
| 71 | // Calculate buffer for horizontal offset due to slanted angle | |
| 72 | const buffer = Math.abs(height * Math.tan(angleRad)) + config.lineLength; | |
| 73 | ||
| 74 | // Adjust the particles array | |
| 75 | if (particles.length < targetParticleCount) { | |
| 76 | // Add more particles if needed | |
| 77 | for (let i = particles.length; i < targetParticleCount; i++) { | |
| 78 | particles.push(createParticle(true, buffer)); | |
| 79 | } | |
| 80 | } else if (particles.length > targetParticleCount) { | |
| 81 | // Remove excess particles | |
| 82 | particles = particles.slice(0, targetParticleCount); | |
| 83 | } | |
| 84 | }; | |
| 85 | ||
| 86 | // Create a new particle | |
| 87 | // Added initialDistribution parameter to distribute particles across the entire canvas at startup | |
| 88 | const createParticle = ( | |
| 89 | initialDistribution = false, | |
| 90 | buffer: number, | |
| 91 | ): RainParticle => { | |
| 92 | // For initial distribution, place particles throughout the canvas | |
| 93 | // Otherwise start them above the canvas | |
| 94 | let x = Math.random() * (width + buffer * 2) - buffer; | |
| 95 | let y; | |
| 96 | ||
| 97 | if (initialDistribution) { | |
| 98 | // Distribute across the entire canvas height for initial setup | |
| 99 | y = Math.random() * (height + config.lineLength * 2) - config.lineLength; | |
| 100 | } else { | |
| 101 | // Start new particles from above the canvas with some randomization | |
| 102 | y = -config.lineLength - (Math.random() * config.lineLength * 20); | |
| 103 | } | |
| 104 | ||
| 105 | return { | |
| 106 | x, | |
| 107 | y, | |
| 108 | }; | |
| 109 | }; | |
| 110 | ||
| 111 | // Update particle positions | |
| 112 | const updateParticles = () => { | |
| 113 | // Calculate buffer for horizontal offset due to slanted angle | |
| 114 | const buffer = Math.abs(height * Math.tan(angleRad)) + config.lineLength; | |
| 115 | ||
| 116 | for (let i = 0; i < particles.length; i++) { | |
| 117 | const p = particles[i]; | |
| 118 | ||
| 119 | // Update position based on speed and angle | |
| 120 | p.x += Math.sin(angleRad) * config.speed; | |
| 121 | p.y += Math.cos(angleRad) * config.speed; | |
| 122 | ||
| 123 | // Reset particles that go offscreen - only determined by position | |
| 124 | // Add extra buffer to ensure particles fully exit the visible area before resetting | |
| 125 | if ( | |
| 126 | p.y > height + config.lineLength || | |
| 127 | p.x < -buffer || | |
| 128 | p.x > width + buffer | |
| 129 | ) { | |
| 130 | particles[i] = createParticle(false, buffer); | |
| 131 | } | |
| 132 | } | |
| 133 | }; | |
| 134 | ||
| 135 | // Draw particles | |
| 136 | const drawParticles = () => { | |
| 137 | // Clear canvas | |
| 138 | ctx.clearRect(0, 0, width, height); | |
| 139 | ||
| 140 | // Set drawing properties | |
| 141 | ctx.strokeStyle = config.color; | |
| 142 | ctx.lineWidth = config.lineWidth; | |
| 143 | ctx.lineCap = "square"; | |
| 144 | ||
| 145 | // Draw each rain line | |
| 146 | ctx.beginPath(); | |
| 147 | for (const p of particles) { | |
| 148 | // Only draw particles that are either on screen or within a reasonable buffer | |
| 149 | // This is for performance reasons - we don't need to draw particles far offscreen | |
| 150 | if (p.y >= -config.lineLength * 2 && p.y <= height + config.lineLength) { | |
| 151 | const endX = p.x + Math.sin(angleRad) * config.lineLength; | |
| 152 | const endY = p.y + Math.cos(angleRad) * config.lineLength; | |
| 153 | ||
| 154 | ctx.moveTo(p.x, p.y); | |
| 155 | ctx.lineTo(endX, endY); | |
| 156 | } | |
| 157 | } | |
| 158 | ctx.stroke(); | |
| 159 | }; | |
| 160 | ||
| 161 | // Animation loop | |
| 162 | const animate = (currentTime: number) => { | |
| 163 | animationFrameId = requestAnimationFrame(animate); | |
| 164 | ||
| 165 | // Control frame rate | |
| 166 | if (currentTime - lastFrameTime < frameInterval) { | |
| 167 | return; | |
| 168 | } | |
| 169 | ||
| 170 | lastFrameTime = currentTime; | |
| 171 | ||
| 172 | updateParticles(); | |
| 173 | drawParticles(); | |
| 174 | }; | |
| 175 | ||
| 176 | // Initialize the animation | |
| 177 | const init = () => { | |
| 178 | // Set up resize handler | |
| 179 | globalThis.addEventListener("resize", updateDimensions); | |
| 180 | ||
| 181 | // Initial setup | |
| 182 | updateDimensions(); | |
| 183 | ||
| 184 | // Start animation | |
| 185 | lastFrameTime = performance.now(); | |
| 186 | animationFrameId = requestAnimationFrame(animate); | |
| 187 | }; | |
| 188 | ||
| 189 | // Start the animation | |
| 190 | init(); | |
| 191 | ||
| 192 | // Return cleanup function | |
| 193 | return () => { | |
| 194 | globalThis.removeEventListener("resize", updateDimensions); | |
| 195 | cancelAnimationFrame(animationFrameId); | |
| 196 | }; | |
| 197 | }; |
src/file-viewer/scripts/canvas_2021.client.ts created+783| ... | ... | @@ -0,0 +1,783 @@ |
| 1 | // Vibe coded. | |
| 2 | (globalThis as any).canvas_2021 = function (canvas: HTMLCanvasElement) { | |
| 3 | const isStandalone = canvas.getAttribute("data-standalone") === "true"; | |
| 4 | // Constants for simulation | |
| 5 | const PARTICLE_RADIUS = 4.5; | |
| 6 | const PARTICLE_DENSITY = 0.004; // Particles per pixel | |
| 7 | const MIN_SPEED = 0.05; | |
| 8 | const MAX_SPEED = 6.0; | |
| 9 | const FRICTION = 0.96; | |
| 10 | const REPULSION_STRENGTH = 0.1; | |
| 11 | const REPULSION_RADIUS = 50; | |
| 12 | const FORCE_RADIUS = 400; // Increased radius | |
| 13 | const FORCE_STRENGTH = 0.25; | |
| 14 | const FORCE_FALLOFF_EXPONENT = 3; // Higher value = sharper falloff | |
| 15 | const FORCE_SPACING = 10; // Pixels between force points | |
| 16 | const MIN_FORCE_STRENGTH = 0.05; // Minimum force strength for very slow movements | |
| 17 | const MAX_FORCE_STRENGTH = 0.4; // Maximum force strength for fast movements | |
| 18 | const MIN_SPEED_THRESHOLD = 1; // Movement speed (px/frame) that produces minimum force | |
| 19 | const MAX_SPEED_THRESHOLD = 20; // Movement speed that produces maximum force | |
| 20 | const OVERSCAN_PIXELS = 250; | |
| 21 | const CELL_SIZE = REPULSION_RADIUS; // For spatial hashing | |
| 22 | ||
| 23 | let globalOpacity = 0; | |
| 24 | ||
| 25 | if (isStandalone) { | |
| 26 | canvas.style.backgroundColor = "#301D02"; | |
| 27 | } else { | |
| 28 | canvas.style.backgroundColor = "transparent"; | |
| 29 | } | |
| 30 | ||
| 31 | // Interfaces | |
| 32 | interface Particle { | |
| 33 | x: number; | |
| 34 | y: number; | |
| 35 | vx: number; | |
| 36 | vy: number; | |
| 37 | charge: number; // 0 to 1, affecting color | |
| 38 | } | |
| 39 | ||
| 40 | interface Force { | |
| 41 | x: number; | |
| 42 | y: number; | |
| 43 | dx: number; | |
| 44 | dy: number; | |
| 45 | strength: number; | |
| 46 | radius: number; | |
| 47 | createdAt: number; | |
| 48 | } | |
| 49 | ||
| 50 | interface SpatialHash { | |
| 51 | [key: string]: Particle[]; | |
| 52 | } | |
| 53 | ||
| 54 | // State | |
| 55 | let first = true; | |
| 56 | let particles: Particle[] = []; | |
| 57 | let forces: Force[] = []; | |
| 58 | let width = canvas.width; | |
| 59 | let height = canvas.height; | |
| 60 | let targetParticleCount = 0; | |
| 61 | let spatialHash: SpatialHash = {}; | |
| 62 | let ctx: CanvasRenderingContext2D | null = null; | |
| 63 | let animationId: number | null = null; | |
| 64 | let isRunning = false; | |
| 65 | ||
| 66 | // Mouse tracking | |
| 67 | let lastMousePosition: { x: number; y: number } | null = null; | |
| 68 | // Track position of the last created force | |
| 69 | let lastForcePosition: { x: number; y: number } | null = null; | |
| 70 | ||
| 71 | // Keep track of previous canvas dimensions for resize logic | |
| 72 | let previousWidth = 0; | |
| 73 | let previousHeight = 0; | |
| 74 | ||
| 75 | // Initialize and cleanup | |
| 76 | function init(): void { | |
| 77 | ctx = canvas.getContext("2d"); | |
| 78 | if (!ctx) return; | |
| 79 | ||
| 80 | // Set canvas to full size | |
| 81 | resizeCanvas(); | |
| 82 | ||
| 83 | // Event listeners | |
| 84 | globalThis.addEventListener("resize", resizeCanvas); | |
| 85 | document.addEventListener("mousemove", handleMouseMove); | |
| 86 | ||
| 87 | // Start animation immediately | |
| 88 | start(); | |
| 89 | } | |
| 90 | ||
| 91 | function cleanup(): void { | |
| 92 | // Stop the animation | |
| 93 | stop(); | |
| 94 | ||
| 95 | // Remove event listeners | |
| 96 | globalThis.removeEventListener("resize", resizeCanvas); | |
| 97 | document.removeEventListener("mousemove", handleMouseMove); | |
| 98 | ||
| 99 | // Clear arrays | |
| 100 | particles = []; | |
| 101 | forces = []; | |
| 102 | spatialHash = {}; | |
| 103 | lastMousePosition = null; | |
| 104 | lastForcePosition = null; | |
| 105 | } | |
| 106 | ||
| 107 | // Resize canvas and adjust particle count | |
| 108 | function resizeCanvas(): void { | |
| 109 | // Store previous dimensions | |
| 110 | previousWidth = width; | |
| 111 | previousHeight = height; | |
| 112 | ||
| 113 | // Update to new dimensions | |
| 114 | width = globalThis.innerWidth; | |
| 115 | height = globalThis.innerHeight; | |
| 116 | canvas.width = width; | |
| 117 | canvas.height = height; | |
| 118 | ||
| 119 | const oldTargetCount = targetParticleCount; | |
| 120 | targetParticleCount = Math.floor(width * height * PARTICLE_DENSITY); | |
| 121 | ||
| 122 | // Adjust particle count | |
| 123 | if (targetParticleCount > oldTargetCount) { | |
| 124 | // Add more particles if needed, but only in newly available space | |
| 125 | addParticles(targetParticleCount - oldTargetCount, !first); | |
| 126 | first = false; | |
| 127 | } | |
| 128 | // Note: Removal of excess particles happens naturally during update | |
| 129 | } | |
| 130 | ||
| 131 | // Handle mouse movement | |
| 132 | function handleMouseMove(e: MouseEvent): void { | |
| 133 | const rect = canvas.getBoundingClientRect(); | |
| 134 | const currentX = e.clientX - rect.left; | |
| 135 | const currentY = e.clientY - rect.top; | |
| 136 | ||
| 137 | // Initialize positions if this is the first movement | |
| 138 | if (!lastMousePosition || !lastForcePosition) { | |
| 139 | lastMousePosition = { x: currentX, y: currentY }; | |
| 140 | lastForcePosition = { x: currentX, y: currentY }; | |
| 141 | return; | |
| 142 | } | |
| 143 | ||
| 144 | // Store current mouse position | |
| 145 | const mouseX = currentX; | |
| 146 | const mouseY = currentY; | |
| 147 | ||
| 148 | // Calculate vector from last mouse position to current | |
| 149 | const dx = mouseX - lastMousePosition.x; | |
| 150 | const dy = mouseY - lastMousePosition.y; | |
| 151 | const distMoved = Math.sqrt(dx * dx + dy * dy); | |
| 152 | ||
| 153 | // Skip if essentially no movement (avoids numerical issues) | |
| 154 | if (distMoved < 0.1) { | |
| 155 | return; | |
| 156 | } | |
| 157 | ||
| 158 | // Get the vector from the last force to the current mouse position | |
| 159 | const forceDx = mouseX - lastForcePosition.x; | |
| 160 | const forceDy = mouseY - lastForcePosition.y; | |
| 161 | const forceDistance = Math.sqrt(forceDx * forceDx + forceDy * forceDy); | |
| 162 | ||
| 163 | // Only create forces if we've moved far enough from the last force | |
| 164 | if (forceDistance >= FORCE_SPACING) { | |
| 165 | // Calculate the direction vector from last force to current mouse | |
| 166 | let dirX = forceDx / forceDistance; | |
| 167 | let dirY = forceDy / forceDistance; | |
| 168 | ||
| 169 | // Calculate how many force points to create | |
| 170 | const numPoints = Math.floor(forceDistance / FORCE_SPACING); | |
| 171 | ||
| 172 | // Calculate movement speed based on the recent movement | |
| 173 | const movementSpeed = distMoved; // Simple approximation of speed | |
| 174 | ||
| 175 | // Scale force strength based on movement speed | |
| 176 | let speedFactor; | |
| 177 | if (movementSpeed <= MIN_SPEED_THRESHOLD) { | |
| 178 | speedFactor = MIN_FORCE_STRENGTH; | |
| 179 | } else if (movementSpeed >= MAX_SPEED_THRESHOLD) { | |
| 180 | speedFactor = MAX_FORCE_STRENGTH; | |
| 181 | } else { | |
| 182 | // Linear interpolation between min and max | |
| 183 | const t = (movementSpeed - MIN_SPEED_THRESHOLD) / | |
| 184 | (MAX_SPEED_THRESHOLD - MIN_SPEED_THRESHOLD); | |
| 185 | speedFactor = MIN_FORCE_STRENGTH + | |
| 186 | t * (MAX_FORCE_STRENGTH - MIN_FORCE_STRENGTH); | |
| 187 | } | |
| 188 | ||
| 189 | // Store current force position to update incrementally | |
| 190 | let currentForceX = lastForcePosition.x; | |
| 191 | let currentForceY = lastForcePosition.y; | |
| 192 | ||
| 193 | // Create evenly spaced force points along the path from last force to current mouse | |
| 194 | for (let i = 0; i < numPoints; i++) { | |
| 195 | // Calculate position for this force point | |
| 196 | const t = (i + 1) / numPoints; | |
| 197 | const fx = lastForcePosition.x + forceDx * t; | |
| 198 | const fy = lastForcePosition.y + forceDy * t; | |
| 199 | ||
| 200 | // Create force at this position with the direction vector | |
| 201 | createForce(fx, fy, dirX, dirY, speedFactor); | |
| 202 | ||
| 203 | // Update the last force position to this new force | |
| 204 | currentForceX = fx; | |
| 205 | currentForceY = fy; | |
| 206 | } | |
| 207 | ||
| 208 | // Update the last force position | |
| 209 | lastForcePosition = { x: currentForceX, y: currentForceY }; | |
| 210 | } | |
| 211 | ||
| 212 | // Always update the last mouse position | |
| 213 | lastMousePosition = { x: mouseX, y: mouseY }; | |
| 214 | } | |
| 215 | ||
| 216 | // Create a new force | |
| 217 | function createForce( | |
| 218 | x: number, | |
| 219 | y: number, | |
| 220 | dx: number, | |
| 221 | dy: number, | |
| 222 | strength = FORCE_STRENGTH, | |
| 223 | ): void { | |
| 224 | forces.push({ | |
| 225 | x, | |
| 226 | y, | |
| 227 | dx, | |
| 228 | dy, | |
| 229 | strength, | |
| 230 | radius: 1, | |
| 231 | createdAt: Date.now(), | |
| 232 | }); | |
| 233 | } | |
| 234 | ||
| 235 | // Improved particle addition with fill strategy options | |
| 236 | function addParticles(count: number, inNewAreaOnly: boolean = false): void { | |
| 237 | // Determine available space | |
| 238 | const minX = -OVERSCAN_PIXELS; | |
| 239 | const maxX = width + OVERSCAN_PIXELS; | |
| 240 | const minY = -OVERSCAN_PIXELS; | |
| 241 | const maxY = height + OVERSCAN_PIXELS; | |
| 242 | ||
| 243 | // Use a grid system that guarantees uniform spacing of particles | |
| 244 | const gridSpacing = REPULSION_RADIUS * 0.8; // Slightly less than repulsion radius | |
| 245 | const gridWidth = Math.ceil((maxX - minX) / gridSpacing); | |
| 246 | const gridHeight = Math.ceil((maxY - minY) / gridSpacing); | |
| 247 | ||
| 248 | // Track which grid cells are already occupied | |
| 249 | const occupiedCells: Set<string> = new Set(); | |
| 250 | ||
| 251 | // Mark cells occupied by existing particles | |
| 252 | for (const particle of particles) { | |
| 253 | const cellX = Math.floor((particle.x - minX) / gridSpacing); | |
| 254 | const cellY = Math.floor((particle.y - minY) / gridSpacing); | |
| 255 | ||
| 256 | // Ensure cell coordinates are within valid range | |
| 257 | if (cellX >= 0 && cellX < gridWidth && cellY >= 0 && cellY < gridHeight) { | |
| 258 | occupiedCells.add(`${cellX},${cellY}`); | |
| 259 | } | |
| 260 | } | |
| 261 | ||
| 262 | // Create arrays of all cells and filter by placement strategy | |
| 263 | const allGridCells: { x: number; y: number }[] = []; | |
| 264 | ||
| 265 | for (let cellY = 0; cellY < gridHeight; cellY++) { | |
| 266 | for (let cellX = 0; cellX < gridWidth; cellX++) { | |
| 267 | const cellKey = `${cellX},${cellY}`; | |
| 268 | if (!occupiedCells.has(cellKey)) { | |
| 269 | const posX = minX + (cellX + 0.5) * gridSpacing; | |
| 270 | const posY = minY + (cellY + 0.5) * gridSpacing; | |
| 271 | ||
| 272 | // For new area only placement, filter to expanded areas | |
| 273 | if (inNewAreaOnly && previousWidth > 0 && previousHeight > 0) { | |
| 274 | const expandedRight = width > previousWidth; | |
| 275 | const expandedBottom = height > previousHeight; | |
| 276 | ||
| 277 | const inNewRightArea = expandedRight && posX >= previousWidth && | |
| 278 | posX <= width; | |
| 279 | const inNewBottomArea = expandedBottom && posY >= previousHeight && | |
| 280 | posY <= height; | |
| 281 | ||
| 282 | if (inNewRightArea || inNewBottomArea) { | |
| 283 | allGridCells.push({ x: cellX, y: cellY }); | |
| 284 | } | |
| 285 | } else if (!inNewAreaOnly) { | |
| 286 | // Standard placement - add all valid cells | |
| 287 | allGridCells.push({ x: cellX, y: cellY }); | |
| 288 | } | |
| 289 | } | |
| 290 | } | |
| 291 | } | |
| 292 | ||
| 293 | if (allGridCells.length == 0) { | |
| 294 | throw new Error("No cells available to place particles"); | |
| 295 | } | |
| 296 | ||
| 297 | // We now have all grid cells that match our placement criteria | |
| 298 | ||
| 299 | // If we need more particles than we have available cells, we need to adjust | |
| 300 | // gridSpacing to fit more cells into the same space | |
| 301 | if (count > allGridCells.length) { | |
| 302 | // Retry with a smaller grid spacing | |
| 303 | // Proportionally reduce the grid spacing to fit the required number of particles | |
| 304 | const scaleFactor = Math.sqrt(allGridCells.length / count); | |
| 305 | const newGridSpacing = gridSpacing * scaleFactor; | |
| 306 | ||
| 307 | // Clear particles and try again with new spacing | |
| 308 | // This is a recursive call, but with adjusted parameters that will fit | |
| 309 | return addParticlesWithCustomSpacing( | |
| 310 | count, | |
| 311 | inNewAreaOnly, | |
| 312 | newGridSpacing, | |
| 313 | ); | |
| 314 | } | |
| 315 | ||
| 316 | // Shuffle the available cells for random selection | |
| 317 | shuffleArray(allGridCells); | |
| 318 | ||
| 319 | // Take the number of cells we need | |
| 320 | const cellsToUse = Math.min(count, allGridCells.length); | |
| 321 | const selectedCells = allGridCells.slice(0, cellsToUse); | |
| 322 | ||
| 323 | // Create particles in selected cells | |
| 324 | for (const cell of selectedCells) { | |
| 325 | // Add jitter within the cell for natural look | |
| 326 | const jitterX = (Math.random() - 0.5) * gridSpacing * 0.8; | |
| 327 | const jitterY = (Math.random() - 0.5) * gridSpacing * 0.8; | |
| 328 | ||
| 329 | // Calculate final position | |
| 330 | const x = minX + (cell.x + 0.5) * gridSpacing + jitterX; | |
| 331 | const y = minY + (cell.y + 0.5) * gridSpacing + jitterY; | |
| 332 | ||
| 333 | // Create a particle at this position | |
| 334 | particles.push(createParticle(x, y)); | |
| 335 | } | |
| 336 | } | |
| 337 | ||
| 338 | // Helper function to add particles with custom grid spacing | |
| 339 | function addParticlesWithCustomSpacing( | |
| 340 | count: number, | |
| 341 | inNewAreaOnly: boolean, | |
| 342 | gridSpacing: number, | |
| 343 | ): void { | |
| 344 | if (gridSpacing == 0) throw new Error("Grid spacing is 0"); | |
| 345 | // Determine available space | |
| 346 | const minX = -OVERSCAN_PIXELS; | |
| 347 | const maxX = width + OVERSCAN_PIXELS; | |
| 348 | const minY = -OVERSCAN_PIXELS; | |
| 349 | const maxY = height + OVERSCAN_PIXELS; | |
| 350 | ||
| 351 | // Create grid using the custom spacing | |
| 352 | const gridWidth = Math.ceil((maxX - minX) / gridSpacing); | |
| 353 | const gridHeight = Math.ceil((maxY - minY) / gridSpacing); | |
| 354 | ||
| 355 | // Track which grid cells are already occupied | |
| 356 | const occupiedCells: Set<string> = new Set(); | |
| 357 | ||
| 358 | // Mark cells occupied by existing particles | |
| 359 | for (const particle of particles) { | |
| 360 | const cellX = Math.floor((particle.x - minX) / gridSpacing); | |
| 361 | const cellY = Math.floor((particle.y - minY) / gridSpacing); | |
| 362 | ||
| 363 | // Ensure cell coordinates are within valid range | |
| 364 | if (cellX >= 0 && cellX < gridWidth && cellY >= 0 && cellY < gridHeight) { | |
| 365 | occupiedCells.add(`${cellX},${cellY}`); | |
| 366 | } | |
| 367 | } | |
| 368 | ||
| 369 | // Create arrays of all cells and filter by placement strategy | |
| 370 | const allGridCells: { x: number; y: number }[] = []; | |
| 371 | ||
| 372 | for (let cellY = 0; cellY < gridHeight; cellY++) { | |
| 373 | for (let cellX = 0; cellX < gridWidth; cellX++) { | |
| 374 | const cellKey = `${cellX},${cellY}`; | |
| 375 | if (!occupiedCells.has(cellKey)) { | |
| 376 | const posX = minX + (cellX + 0.5) * gridSpacing; | |
| 377 | const posY = minY + (cellY + 0.5) * gridSpacing; | |
| 378 | ||
| 379 | // For new area only placement, filter to expanded areas | |
| 380 | if (inNewAreaOnly && previousWidth > 0 && previousHeight > 0) { | |
| 381 | const expandedRight = width > previousWidth; | |
| 382 | const expandedBottom = height > previousHeight; | |
| 383 | ||
| 384 | const inNewRightArea = expandedRight && posX >= previousWidth && | |
| 385 | posX <= width; | |
| 386 | const inNewBottomArea = expandedBottom && posY >= previousHeight && | |
| 387 | posY <= height; | |
| 388 | ||
| 389 | if (inNewRightArea || inNewBottomArea) { | |
| 390 | allGridCells.push({ x: cellX, y: cellY }); | |
| 391 | } | |
| 392 | } else if (!inNewAreaOnly) { | |
| 393 | // Standard placement - add all valid cells | |
| 394 | allGridCells.push({ x: cellX, y: cellY }); | |
| 395 | } | |
| 396 | } | |
| 397 | } | |
| 398 | } | |
| 399 | ||
| 400 | // Shuffle the available cells for random distribution | |
| 401 | shuffleArray(allGridCells); | |
| 402 | ||
| 403 | // Take the number of cells we need (or all if we have fewer) | |
| 404 | const cellsToUse = Math.min(count, allGridCells.length); | |
| 405 | ||
| 406 | // Create particles in selected cells | |
| 407 | for (let i = 0; i < cellsToUse; i++) { | |
| 408 | const cell = allGridCells[i]; | |
| 409 | ||
| 410 | // Add jitter within the cell | |
| 411 | const jitterX = (Math.random() - 0.5) * gridSpacing * 0.8; | |
| 412 | const jitterY = (Math.random() - 0.5) * gridSpacing * 0.8; | |
| 413 | ||
| 414 | // Calculate final position | |
| 415 | const x = minX + (cell.x + 0.5) * gridSpacing + jitterX; | |
| 416 | const y = minY + (cell.y + 0.5) * gridSpacing + jitterY; | |
| 417 | ||
| 418 | // Create a particle at this position | |
| 419 | particles.push(createParticle(x, y)); | |
| 420 | } | |
| 421 | } | |
| 422 | ||
| 423 | // Utility to shuffle an array (Fisher-Yates algorithm) | |
| 424 | function shuffleArray<T>(array: T[]): void { | |
| 425 | for (let i = array.length - 1; i > 0; i--) { | |
| 426 | const j = Math.floor(Math.random() * (i + 1)); | |
| 427 | [array[i], array[j]] = [array[j], array[i]]; | |
| 428 | } | |
| 429 | } | |
| 430 | ||
| 431 | // Simplified createParticle function that just places at a specific position | |
| 432 | function createParticle(x: number, y: number): Particle { | |
| 433 | return { | |
| 434 | x: x + (Math.random() * 4 - 2), | |
| 435 | y: y + (Math.random() * 4 - 2), | |
| 436 | vx: 0, | |
| 437 | vy: 0, | |
| 438 | charge: 0, | |
| 439 | }; | |
| 440 | } | |
| 441 | ||
| 442 | // Function to create a particle on one of the edges | |
| 443 | function createParticleOnEdge(): Particle { | |
| 444 | // Overscan bounds with fixed pixel size | |
| 445 | const minX = -OVERSCAN_PIXELS; | |
| 446 | const maxX = width + OVERSCAN_PIXELS; | |
| 447 | const minY = -OVERSCAN_PIXELS; | |
| 448 | const maxY = height + OVERSCAN_PIXELS; | |
| 449 | ||
| 450 | let x: number, y: number; | |
| 451 | ||
| 452 | // Place on one of the edges | |
| 453 | const edge = Math.floor(Math.random() * 4); | |
| 454 | switch (edge) { | |
| 455 | case 0: // Top | |
| 456 | x = minX + Math.random() * (maxX - minX); | |
| 457 | y = minY; | |
| 458 | break; | |
| 459 | case 1: // Right | |
| 460 | x = maxX; | |
| 461 | y = minY + Math.random() * (maxY - minY); | |
| 462 | break; | |
| 463 | case 2: // Bottom | |
| 464 | x = minX + Math.random() * (maxX - minX); | |
| 465 | y = maxY; | |
| 466 | break; | |
| 467 | case 3: // Left | |
| 468 | x = minX; | |
| 469 | y = minY + Math.random() * (maxY - minY); | |
| 470 | break; | |
| 471 | default: | |
| 472 | x = minX + Math.random() * (maxX - minX); | |
| 473 | y = minY + Math.random() * (maxY - minY); | |
| 474 | } | |
| 475 | ||
| 476 | return createParticle(x, y); | |
| 477 | } | |
| 478 | ||
| 479 | // Spatial hashing functions | |
| 480 | function getHashKey(x: number, y: number): string { | |
| 481 | const cellX = Math.floor(x / CELL_SIZE); | |
| 482 | const cellY = Math.floor(y / CELL_SIZE); | |
| 483 | return `${cellX},${cellY}`; | |
| 484 | } | |
| 485 | ||
| 486 | function addToSpatialHash(particle: Particle): void { | |
| 487 | const key = getHashKey(particle.x, particle.y); | |
| 488 | if (!spatialHash[key]) { | |
| 489 | spatialHash[key] = []; | |
| 490 | } | |
| 491 | spatialHash[key].push(particle); | |
| 492 | } | |
| 493 | ||
| 494 | function updateSpatialHash(): void { | |
| 495 | // Clear previous hash | |
| 496 | spatialHash = {}; | |
| 497 | ||
| 498 | // Add all particles to hash | |
| 499 | for (const particle of particles) { | |
| 500 | addToSpatialHash(particle); | |
| 501 | } | |
| 502 | } | |
| 503 | ||
| 504 | function getNearbyParticles( | |
| 505 | x: number, | |
| 506 | y: number, | |
| 507 | radius: number, | |
| 508 | ): Particle[] { | |
| 509 | const result: Particle[] = []; | |
| 510 | const cellRadius = Math.ceil(radius / CELL_SIZE); | |
| 511 | ||
| 512 | const centerCellX = Math.floor(x / CELL_SIZE); | |
| 513 | const centerCellY = Math.floor(y / CELL_SIZE); | |
| 514 | ||
| 515 | for ( | |
| 516 | let cellX = centerCellX - cellRadius; | |
| 517 | cellX <= centerCellX + cellRadius; | |
| 518 | cellX++ | |
| 519 | ) { | |
| 520 | for ( | |
| 521 | let cellY = centerCellY - cellRadius; | |
| 522 | cellY <= centerCellY + cellRadius; | |
| 523 | cellY++ | |
| 524 | ) { | |
| 525 | const key = `${cellX},${cellY}`; | |
| 526 | const cell = spatialHash[key]; | |
| 527 | ||
| 528 | if (cell) { | |
| 529 | result.push(...cell); | |
| 530 | } | |
| 531 | } | |
| 532 | } | |
| 533 | ||
| 534 | return result; | |
| 535 | } | |
| 536 | ||
| 537 | // Main update function | |
| 538 | function update(): void { | |
| 539 | const now = Date.now(); | |
| 540 | // Fixed pixel overscan | |
| 541 | const minX = -OVERSCAN_PIXELS; | |
| 542 | const maxX = width + OVERSCAN_PIXELS; | |
| 543 | const minY = -OVERSCAN_PIXELS; | |
| 544 | const maxY = height + OVERSCAN_PIXELS; | |
| 545 | ||
| 546 | // Update spatial hash | |
| 547 | updateSpatialHash(); | |
| 548 | ||
| 549 | // Update forces and remove expired ones | |
| 550 | if (forces.length > 40) { | |
| 551 | forces = forces.slice(-40); | |
| 552 | } | |
| 553 | forces = forces.filter((force) => { | |
| 554 | force.strength *= 0.95; | |
| 555 | force.radius *= 0.95; | |
| 556 | return force.strength > 0.001; | |
| 557 | }); | |
| 558 | ||
| 559 | // Update particles | |
| 560 | const newParticles: Particle[] = []; | |
| 561 | ||
| 562 | for (const particle of particles) { | |
| 563 | // Apply forces | |
| 564 | for (const force of forces) { | |
| 565 | const dx = particle.x - force.x; | |
| 566 | const dy = particle.y - force.y; | |
| 567 | const distSq = dx * dx + dy * dy; | |
| 568 | ||
| 569 | const radius = force.radius * FORCE_RADIUS; | |
| 570 | ||
| 571 | if (distSq < radius * radius) { | |
| 572 | const dist = Math.sqrt(distSq); | |
| 573 | ||
| 574 | // Exponential falloff - much more concentrated at center | |
| 575 | // (1 - x/R)^n where n controls how sharp the falloff is | |
| 576 | const normalizedDist = dist / radius; | |
| 577 | const factor = Math.pow(1 - normalizedDist, FORCE_FALLOFF_EXPONENT); | |
| 578 | ||
| 579 | // Calculate force line projection for directional effect | |
| 580 | // This makes particles along the force's path experience stronger effect | |
| 581 | const dotProduct = (dx * -force.dx) + (dy * -force.dy); | |
| 582 | const projectionFactor = Math.max(0, dotProduct / dist); | |
| 583 | ||
| 584 | // Apply the combined factors - stronger directional bias | |
| 585 | const finalFactor = factor * force.strength * | |
| 586 | (0.1 + 0.9 * projectionFactor); | |
| 587 | ||
| 588 | particle.vx += force.dx * finalFactor; | |
| 589 | particle.vy += force.dy * finalFactor; | |
| 590 | // charge for the first 100ms | |
| 591 | if ((now - force.createdAt) < 100) { | |
| 592 | particle.charge = Math.min( | |
| 593 | 1, | |
| 594 | particle.charge + (finalFactor * finalFactor) * 0.2, | |
| 595 | ); | |
| 596 | } | |
| 597 | } | |
| 598 | } | |
| 599 | ||
| 600 | // Apply repulsion from nearby particles | |
| 601 | const nearby = getNearbyParticles( | |
| 602 | particle.x, | |
| 603 | particle.y, | |
| 604 | REPULSION_RADIUS, | |
| 605 | ); | |
| 606 | ||
| 607 | for (const other of nearby) { | |
| 608 | if (other === particle) continue; | |
| 609 | ||
| 610 | const dx = particle.x - other.x; | |
| 611 | const dy = particle.y - other.y; | |
| 612 | const distSq = dx * dx + dy * dy; | |
| 613 | ||
| 614 | if (distSq < REPULSION_RADIUS * REPULSION_RADIUS && distSq > 0) { | |
| 615 | const dist = Math.sqrt(distSq); | |
| 616 | const factor = REPULSION_STRENGTH * (1 - dist / REPULSION_RADIUS); | |
| 617 | ||
| 618 | const fx = dx / dist * factor; | |
| 619 | const fy = dy / dist * factor; | |
| 620 | ||
| 621 | particle.vx += fx; | |
| 622 | particle.vy += fy; | |
| 623 | } | |
| 624 | } | |
| 625 | ||
| 626 | // Apply friction | |
| 627 | particle.vx *= FRICTION; | |
| 628 | particle.vy *= FRICTION; | |
| 629 | ||
| 630 | // Ensure minimum speed | |
| 631 | const speed = Math.sqrt( | |
| 632 | particle.vx * particle.vx + particle.vy * particle.vy, | |
| 633 | ); | |
| 634 | if (speed < MIN_SPEED && speed > 0) { | |
| 635 | const scale = MIN_SPEED / speed; | |
| 636 | particle.vx *= scale; | |
| 637 | particle.vy *= scale; | |
| 638 | } | |
| 639 | ||
| 640 | // Cap at maximum speed | |
| 641 | if (speed > MAX_SPEED) { | |
| 642 | const scale = MAX_SPEED / speed; | |
| 643 | particle.vx *= scale; | |
| 644 | particle.vy *= scale; | |
| 645 | } | |
| 646 | ||
| 647 | // Update position | |
| 648 | particle.x += particle.vx; | |
| 649 | particle.y += particle.vy; | |
| 650 | ||
| 651 | // Decrease charge | |
| 652 | particle.charge *= 0.99; | |
| 653 | ||
| 654 | // Check if particle is within extended bounds | |
| 655 | if ( | |
| 656 | particle.x >= minX && particle.x <= maxX && | |
| 657 | particle.y >= minY && particle.y <= maxY | |
| 658 | ) { | |
| 659 | // If outside screen but within overscan, keep it if we need more particles | |
| 660 | if ( | |
| 661 | (particle.x < 0 || particle.x > width || | |
| 662 | particle.y < 0 || particle.y > height) && | |
| 663 | newParticles.length >= targetParticleCount | |
| 664 | ) { | |
| 665 | continue; | |
| 666 | } | |
| 667 | ||
| 668 | newParticles.push(particle); | |
| 669 | } else { | |
| 670 | // Out of bounds, respawn if needed | |
| 671 | if (newParticles.length < targetParticleCount) { | |
| 672 | newParticles.push(createParticleOnEdge()); | |
| 673 | } | |
| 674 | } | |
| 675 | } | |
| 676 | ||
| 677 | // Add more particles if needed | |
| 678 | while (newParticles.length < targetParticleCount) { | |
| 679 | newParticles.push(createParticleOnEdge()); | |
| 680 | } | |
| 681 | ||
| 682 | particles = newParticles; | |
| 683 | } | |
| 684 | ||
| 685 | // Render function | |
| 686 | const mul = isStandalone ? 0.9 : 0.5; | |
| 687 | const add = isStandalone ? 0.1 : 0.03; | |
| 688 | function render(): void { | |
| 689 | if (!ctx) return; | |
| 690 | ||
| 691 | // Clear canvas | |
| 692 | ctx.clearRect(0, 0, width, height); | |
| 693 | ||
| 694 | // Draw particles | |
| 695 | for (const particle of particles) { | |
| 696 | // Only draw if within canvas bounds (plus a small margin) | |
| 697 | if ( | |
| 698 | particle.x >= -PARTICLE_RADIUS && | |
| 699 | particle.x <= width + PARTICLE_RADIUS && | |
| 700 | particle.y >= -PARTICLE_RADIUS && particle.y <= height + PARTICLE_RADIUS | |
| 701 | ) { | |
| 702 | ctx.beginPath(); | |
| 703 | ctx.arc(particle.x, particle.y, PARTICLE_RADIUS, 0, Math.PI * 2); | |
| 704 | ||
| 705 | // Color based on charge | |
| 706 | ctx.fillStyle = "#FFCB1F"; | |
| 707 | ctx.globalAlpha = (particle.charge * mul + add) * globalOpacity; | |
| 708 | ctx.fill(); | |
| 709 | } | |
| 710 | } | |
| 711 | ||
| 712 | // // Debug: Draw forces and falloff visualization | |
| 713 | // if (ctx) { | |
| 714 | // for (const force of forces) { | |
| 715 | // const R = force.radius * FORCE_RADIUS; | |
| 716 | ||
| 717 | // // Draw force point | |
| 718 | // ctx.beginPath(); | |
| 719 | // ctx.arc(force.x, force.y, 5, 0, Math.PI * 2); | |
| 720 | // ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; | |
| 721 | // ctx.fill(); | |
| 722 | ||
| 723 | // // Draw force direction | |
| 724 | // ctx.beginPath(); | |
| 725 | // ctx.moveTo(force.x, force.y); | |
| 726 | // ctx.lineTo(force.x + force.dx * 20, force.y + force.dy * 20); | |
| 727 | // ctx.strokeStyle = 'red'; | |
| 728 | // ctx.stroke(); | |
| 729 | ||
| 730 | // // Visualize the falloff curve with rings | |
| 731 | // for (let i = 0; i <= 10; i++) { | |
| 732 | // const radius = (R * i) / 10; | |
| 733 | // const normalizedDist = radius / R; | |
| 734 | // const intensity = Math.pow(1 - normalizedDist, FORCE_FALLOFF_EXPONENT); | |
| 735 | ||
| 736 | // ctx.beginPath(); | |
| 737 | // ctx.arc(force.x, force.y, radius, 0, Math.PI * 2); | |
| 738 | // ctx.strokeStyle = `rgba(255, 0, 0, ${intensity * 0.2})`; | |
| 739 | // ctx.stroke(); | |
| 740 | // } | |
| 741 | // } | |
| 742 | // } | |
| 743 | } | |
| 744 | ||
| 745 | // Animation loop | |
| 746 | let r = Math.random(); | |
| 747 | function animate(): void { | |
| 748 | globalOpacity = Math.min(1, globalOpacity + 0.03); | |
| 749 | update(); | |
| 750 | render(); | |
| 751 | ||
| 752 | if (isRunning) { | |
| 753 | animationId = requestAnimationFrame(animate); | |
| 754 | } | |
| 755 | } | |
| 756 | ||
| 757 | // Start/stop functions | |
| 758 | function start(): void { | |
| 759 | if (isRunning) return; | |
| 760 | ||
| 761 | // Calculate target particle count based on canvas size | |
| 762 | targetParticleCount = Math.floor(width * height * PARTICLE_DENSITY); | |
| 763 | ||
| 764 | // Clear any existing particles and create new ones with proper spacing | |
| 765 | particles = []; | |
| 766 | addParticles(targetParticleCount); | |
| 767 | ||
| 768 | isRunning = true; | |
| 769 | animate(); | |
| 770 | } | |
| 771 | ||
| 772 | function stop(): void { | |
| 773 | isRunning = false; | |
| 774 | ||
| 775 | if (animationId !== null) { | |
| 776 | cancelAnimationFrame(animationId); | |
| 777 | animationId = null; | |
| 778 | } | |
| 779 | } | |
| 780 | ||
| 781 | init(); | |
| 782 | return cleanup; | |
| 783 | }; |
src/file-viewer/scripts/canvas_2022.client.ts created+160| ... | ... | @@ -0,0 +1,160 @@ |
| 1 | (globalThis as any).canvas_2022 = function (canvas: HTMLCanvasElement) { | |
| 2 | const isStandalone = canvas.getAttribute("data-standalone") === "true"; | |
| 3 | // Configuration for the grid of rotating squares | |
| 4 | const config = { | |
| 5 | gridRotation: 20, // Overall grid rotation in degrees | |
| 6 | squareSize: 20, // Size of each square | |
| 7 | spacing: 100, // Distance between square centers | |
| 8 | moveSpeedX: 0.01, // Horizontal movement speed (pixels per second) | |
| 9 | moveSpeedY: 0.01, // Vertical movement speed (pixels per second) | |
| 10 | squareColor: "#00220A", // Color of the squares | |
| 11 | squareOpacity: 1, // Opacity of the squares | |
| 12 | ||
| 13 | // Function to determine square rotation based on its coordinates and time | |
| 14 | // Can be adjusted for different patterns | |
| 15 | rotationFunction: (x: number, y: number, time: number): number => { | |
| 16 | // Combination of spatial wave and time-based rotation | |
| 17 | return Math.sin(x * 0.05) * Math.cos(y * 0.05) * 180; | |
| 18 | }, | |
| 19 | }; | |
| 20 | ||
| 21 | // Convert grid rotation to radians | |
| 22 | const gridRotationRad = (config.gridRotation * Math.PI) / 180; | |
| 23 | ||
| 24 | // Get the canvas context | |
| 25 | const ctx = canvas.getContext("2d"); | |
| 26 | if (!ctx) { | |
| 27 | console.error("Could not get canvas context"); | |
| 28 | return () => {}; | |
| 29 | } | |
| 30 | ||
| 31 | // Make canvas transparent | |
| 32 | if (isStandalone) { | |
| 33 | canvas.style.backgroundColor = "#154226"; | |
| 34 | } else { | |
| 35 | canvas.style.backgroundColor = "transparent"; | |
| 36 | } | |
| 37 | ||
| 38 | // Animation variables | |
| 39 | let width = canvas.width; | |
| 40 | let height = canvas.height; | |
| 41 | let offsetX = 0; | |
| 42 | let offsetY = 0; | |
| 43 | let time = 0; | |
| 44 | let animationFrameId: number; | |
| 45 | let lastTime = 0; | |
| 46 | ||
| 47 | // Update canvas dimensions when resized | |
| 48 | const updateDimensions = () => { | |
| 49 | width = canvas.width = canvas.clientWidth; | |
| 50 | height = canvas.height = canvas.clientHeight; | |
| 51 | }; | |
| 52 | ||
| 53 | // Calculate the diagonal length of the canvas (to ensure rotation covers corners) | |
| 54 | const calculateDiagonal = () => { | |
| 55 | return Math.sqrt(width * width + height * height); | |
| 56 | }; | |
| 57 | ||
| 58 | // Draw a single square with rotation | |
| 59 | const drawSquare = (x: number, y: number, size: number, rotation: number) => { | |
| 60 | ctx.save(); | |
| 61 | ||
| 62 | // Move to the center of the square position, rotate, then draw | |
| 63 | ctx.translate(x, y); | |
| 64 | ctx.rotate((rotation * Math.PI) / 180); // Convert rotation degrees to radians | |
| 65 | ||
| 66 | // Draw square centered at position | |
| 67 | ctx.fillRect(-size / 2, -size / 2, size, size); | |
| 68 | ||
| 69 | ctx.restore(); | |
| 70 | }; | |
| 71 | ||
| 72 | // Draw the entire grid of squares | |
| 73 | const drawGrid = () => { | |
| 74 | ctx.clearRect(0, 0, width, height); | |
| 75 | ||
| 76 | // Set drawing properties | |
| 77 | ctx.fillStyle = config.squareColor; | |
| 78 | ctx.globalAlpha = config.squareOpacity; | |
| 79 | ||
| 80 | // Save the current transformation state | |
| 81 | ctx.save(); | |
| 82 | ||
| 83 | // Move to the center of the canvas, rotate the grid, then move back | |
| 84 | const centerX = width / 2; | |
| 85 | const centerY = height / 2; | |
| 86 | ctx.translate(centerX, centerY); | |
| 87 | ctx.rotate(gridRotationRad); | |
| 88 | ||
| 89 | // Calculate how much of the grid to draw based on canvas size | |
| 90 | const diagonal = calculateDiagonal(); | |
| 91 | const gridSize = Math.ceil(diagonal / config.spacing) + 2; | |
| 92 | ||
| 93 | // Adjust for offset to create movement | |
| 94 | const adjustedOffsetX = offsetX % config.spacing; | |
| 95 | const adjustedOffsetY = offsetY % config.spacing; | |
| 96 | ||
| 97 | // Draw grid with enough squares to cover the rotated canvas | |
| 98 | const halfGrid = Math.ceil(gridSize / 2); | |
| 99 | ||
| 100 | for (let y = -halfGrid; y <= halfGrid; y++) { | |
| 101 | for (let x = -halfGrid; x <= halfGrid; x++) { | |
| 102 | // Calculate actual position with offset | |
| 103 | const posX = x * config.spacing + adjustedOffsetX; | |
| 104 | const posY = y * config.spacing + adjustedOffsetY; | |
| 105 | ||
| 106 | // Calculate square rotation based on its position and time | |
| 107 | const squareRotation = config.rotationFunction(posX, posY, time); | |
| 108 | ||
| 109 | // Draw the square | |
| 110 | drawSquare(posX, posY, config.squareSize, squareRotation); | |
| 111 | } | |
| 112 | } | |
| 113 | ||
| 114 | // Restore the transformation state | |
| 115 | ctx.restore(); | |
| 116 | ||
| 117 | // Reset global alpha | |
| 118 | ctx.globalAlpha = 1.0; | |
| 119 | }; | |
| 120 | ||
| 121 | // Animation loop | |
| 122 | const animate = (currentTime: number) => { | |
| 123 | animationFrameId = requestAnimationFrame(animate); | |
| 124 | ||
| 125 | // Calculate time elapsed since last frame | |
| 126 | const elapsed = currentTime - lastTime; | |
| 127 | lastTime = currentTime; | |
| 128 | ||
| 129 | // Update time variable for rotation function | |
| 130 | time += elapsed; | |
| 131 | ||
| 132 | // Update position offsets for movement | |
| 133 | offsetX += config.moveSpeedX * elapsed; | |
| 134 | offsetY += config.moveSpeedY * elapsed; | |
| 135 | ||
| 136 | // Draw the grid | |
| 137 | drawGrid(); | |
| 138 | }; | |
| 139 | ||
| 140 | // Initialize the animation | |
| 141 | const init = () => { | |
| 142 | // Set up resize handler | |
| 143 | globalThis.addEventListener("resize", updateDimensions); | |
| 144 | ||
| 145 | // Initial setup | |
| 146 | updateDimensions(); | |
| 147 | ||
| 148 | // Start animation | |
| 149 | animationFrameId = requestAnimationFrame(animate); | |
| 150 | }; | |
| 151 | ||
| 152 | // Start the animation | |
| 153 | init(); | |
| 154 | ||
| 155 | // Return cleanup function | |
| 156 | return () => { | |
| 157 | globalThis.removeEventListener("resize", updateDimensions); | |
| 158 | cancelAnimationFrame(animationFrameId); | |
| 159 | }; | |
| 160 | }; |
src/file-viewer/scripts/canvas_2023.client.ts created+197| ... | ... | @@ -0,0 +1,197 @@ |
| 1 | (globalThis as any).canvas_2023 = function (canvas: HTMLCanvasElement) { | |
| 2 | const isStandalone = canvas.getAttribute("data-standalone") === "true"; | |
| 3 | const config = { | |
| 4 | heartBaseSize: 50, | |
| 5 | heartMaxSize: 100, | |
| 6 | spacing: 150, | |
| 7 | rowSpeed: 0.1, | |
| 8 | heartColor: "#FF90D9", | |
| 9 | heartOpacity: isStandalone ? 0.5 : 0.04, | |
| 10 | mouseInfluenceRadius: 1000, | |
| 11 | heartScaleFunction: (distance: number, radius: number): number => { | |
| 12 | if (distance > radius) return 1; | |
| 13 | ||
| 14 | const normalizedDistance = distance / radius; | |
| 15 | const scaleFactor = 1 + | |
| 16 | (1 - normalizedDistance) * | |
| 17 | (config.heartMaxSize / config.heartBaseSize - 1); | |
| 18 | ||
| 19 | return 1 + (scaleFactor - 1) * Math.pow(1 - normalizedDistance, 2); | |
| 20 | }, | |
| 21 | }; | |
| 22 | ||
| 23 | const heart = new Path2D( | |
| 24 | "M23.9451 45.3973L20.8672 42.6493C16.9551 39.0174 13.7054 35.8927 11.1181 33.275C8.53056 30.6574 6.46731 28.286 4.92839 26.1608C3.38946 24.0356 2.31772 22.1028 1.71314 20.3624C1.10856 18.6219 0.806274 16.8705 0.806274 15.1081C0.806274 11.4718 2.03118 8.42016 4.481 5.95312C6.93118 3.48608 9.93831 2.25256 13.5024 2.25256C15.5649 2.25256 17.482 2.70142 19.2536 3.59912C21.0255 4.49682 22.5893 5.80674 23.9451 7.52887C25.484 5.73346 27.1059 4.40522 28.8108 3.54416C30.5161 2.6831 32.3751 2.25256 34.3877 2.25256C38.0141 2.25256 41.0551 3.48663 43.5108 5.95477C45.9661 8.42291 47.1938 11.4758 47.1938 15.1136C47.1938 16.8712 46.8823 18.6115 46.2594 20.3343C45.6365 22.0568 44.5648 23.9807 43.0442 26.1059C41.5236 28.231 39.4721 30.6136 36.8896 33.2536C34.3068 35.8936 31.0362 39.0255 27.0779 42.6493L23.9451 45.3973ZM23.9176 38.802C27.6088 35.431 30.6339 32.5547 32.9928 30.173C35.3518 27.7913 37.2091 25.7211 38.5648 23.9624C39.9205 22.2036 40.864 20.6137 41.3953 19.1928C41.9266 17.7715 42.1923 16.4101 42.1923 15.1086C42.1923 12.8768 41.4529 11.0098 39.974 9.50748C38.4952 8.0052 36.6461 7.25406 34.4268 7.25406C32.631 7.25406 30.9572 7.6811 29.4055 8.87193C27.8537 10.0628 25.5389 13.0434 25.5389 13.0434L23.9451 15.3299L22.3512 13.0434C22.3512 13.0434 20.0643 10.2311 18.4638 9.04031C16.8634 7.84948 15.2194 7.25406 13.4991 7.25406C11.2929 7.25406 9.46857 7.98816 8.02602 9.45637C6.58383 10.9246 5.86273 12.8162 5.86273 15.1311C5.86273 16.4784 6.13644 17.8679 6.68386 19.2994C7.23127 20.731 8.18394 22.3333 9.54185 24.1064C10.8998 25.879 12.7329 27.9562 15.0413 30.3379C17.3497 32.7196 20.3084 35.5409 23.9176 38.802Z", | |
| 25 | ); | |
| 26 | ||
| 27 | const ctx = canvas.getContext("2d"); | |
| 28 | if (!ctx) { | |
| 29 | console.error("Could not get canvas context"); | |
| 30 | return () => {}; | |
| 31 | } | |
| 32 | ||
| 33 | if (isStandalone) { | |
| 34 | canvas.style.backgroundColor = "#2F1C21"; | |
| 35 | } else { | |
| 36 | canvas.style.backgroundColor = "transparent"; | |
| 37 | } | |
| 38 | ||
| 39 | let width = canvas.width; | |
| 40 | let height = canvas.height; | |
| 41 | let animationFrameId: number; | |
| 42 | let lastFrameTime = 0; | |
| 43 | ||
| 44 | let mouseX = width / 2; | |
| 45 | let mouseY = height / 2; | |
| 46 | ||
| 47 | let offset = config.spacing / 2; | |
| 48 | ||
| 49 | const updateDimensions = () => { | |
| 50 | width = canvas.width = canvas.clientWidth; | |
| 51 | height = canvas.height = canvas.clientHeight; | |
| 52 | ||
| 53 | mouseX = width / 2; | |
| 54 | mouseY = height / 2; | |
| 55 | }; | |
| 56 | ||
| 57 | const drawHeart = (x: number, y: number, size: number) => { | |
| 58 | const scale = size / 30; | |
| 59 | ||
| 60 | ctx.save(); | |
| 61 | ctx.translate(x, y); | |
| 62 | ctx.scale(scale, scale); | |
| 63 | ||
| 64 | ctx.fillStyle = config.heartColor; | |
| 65 | ctx.fill(heart); | |
| 66 | ||
| 67 | ctx.restore(); | |
| 68 | }; | |
| 69 | ||
| 70 | const c = 400; | |
| 71 | const h = 40; | |
| 72 | const k = solveForK(c, h); | |
| 73 | ||
| 74 | const drawHeartGrid = () => { | |
| 75 | ctx.clearRect(0, 0, width, height); | |
| 76 | ||
| 77 | ctx.globalAlpha = config.heartOpacity; | |
| 78 | ||
| 79 | const numRows = Math.ceil(height / config.spacing) + 1; | |
| 80 | ||
| 81 | for (let row = 0; row < numRows; row++) { | |
| 82 | const direction = row % 2 === 0 ? 1 : -1; | |
| 83 | const rowOffset = (offset * direction) % config.spacing; | |
| 84 | ||
| 85 | const posYInit = row * config.spacing + config.spacing / 2; | |
| 86 | ||
| 87 | for ( | |
| 88 | let posXInit = -config.spacing + rowOffset; | |
| 89 | posXInit < width + config.spacing; | |
| 90 | posXInit += config.spacing | |
| 91 | ) { | |
| 92 | const dx = (posXInit + config.heartBaseSize / 2) - mouseX; | |
| 93 | const dy = (posYInit + config.heartBaseSize / 2) - mouseY; | |
| 94 | const distance = Math.sqrt(dx * dx + dy * dy); | |
| 95 | ||
| 96 | const pushIntensity = asymmetricBump(distance, h, c, k, 0.00002); | |
| 97 | ||
| 98 | const pushAngle = Math.atan2(dy, dx); | |
| 99 | ||
| 100 | const pushDistanceX = pushIntensity * Math.cos(pushAngle); | |
| 101 | const pushDistanceY = pushIntensity * Math.sin(pushAngle); | |
| 102 | const posX = posXInit + pushDistanceX * 1; | |
| 103 | const posY = posYInit + pushDistanceY * 2; | |
| 104 | ||
| 105 | const scaleFactor = config.heartScaleFunction( | |
| 106 | distance, | |
| 107 | config.mouseInfluenceRadius, | |
| 108 | ); | |
| 109 | const heartSize = config.heartBaseSize * scaleFactor; | |
| 110 | ||
| 111 | if ( | |
| 112 | posX > -config.heartMaxSize && | |
| 113 | posX < width + config.heartMaxSize && | |
| 114 | posY > -config.heartMaxSize && | |
| 115 | posY < height + config.heartMaxSize | |
| 116 | ) { | |
| 117 | drawHeart(posX - heartSize / 2, posY - heartSize / 2, heartSize); | |
| 118 | } | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | ctx.globalAlpha = 1.0; | |
| 123 | }; | |
| 124 | ||
| 125 | function solveForK(c: number, k: number) { | |
| 126 | // input -> f(x)=h*e^{(-k*(x-c)^{2})} | |
| 127 | // desired result is (0, 0.45). (0, 0) is unsolvable but 0.45px will round down to 0. | |
| 128 | // | |
| 129 | // solution: -\frac{\ln\left(\frac{0.45}{h}\right)}{c^{2}} | |
| 130 | return -Math.log(0.45 / h) / (c * c); | |
| 131 | } | |
| 132 | ||
| 133 | function asymmetricBump( | |
| 134 | x: number, | |
| 135 | h: number, | |
| 136 | c: number, | |
| 137 | leftK: number, | |
| 138 | rightK: number, | |
| 139 | ) { | |
| 140 | const k = (x <= c) ? leftK : rightK; | |
| 141 | return h * Math.exp(-k * Math.pow(x - c, 2)); | |
| 142 | } | |
| 143 | ||
| 144 | const updateOffset = (elapsed: number) => { | |
| 145 | offset += config.rowSpeed * elapsed; | |
| 146 | ||
| 147 | if (offset > 1000000) { | |
| 148 | offset -= 1000000; | |
| 149 | } | |
| 150 | }; | |
| 151 | ||
| 152 | const animate = (currentTime: number) => { | |
| 153 | animationFrameId = requestAnimationFrame(animate); | |
| 154 | ||
| 155 | const elapsed = currentTime - lastFrameTime; | |
| 156 | lastFrameTime = currentTime; | |
| 157 | ||
| 158 | updateOffset(elapsed * 0.05); | |
| 159 | ||
| 160 | drawHeartGrid(); | |
| 161 | }; | |
| 162 | ||
| 163 | const handleMouseMove = (event: MouseEvent) => { | |
| 164 | const rect = canvas.getBoundingClientRect(); | |
| 165 | mouseX = event.clientX - rect.left; | |
| 166 | mouseY = event.clientY - rect.top; | |
| 167 | }; | |
| 168 | ||
| 169 | const handleTouchMove = (event: TouchEvent) => { | |
| 170 | if (event.touches.length > 0) { | |
| 171 | event.preventDefault(); | |
| 172 | const rect = canvas.getBoundingClientRect(); | |
| 173 | mouseX = event.touches[0].clientX - rect.left; | |
| 174 | mouseY = event.touches[0].clientY - rect.top; | |
| 175 | } | |
| 176 | }; | |
| 177 | ||
| 178 | const init = () => { | |
| 179 | globalThis.addEventListener("resize", updateDimensions); | |
| 180 | document.addEventListener("mousemove", handleMouseMove); | |
| 181 | document.addEventListener("touchmove", handleTouchMove, { passive: false }); | |
| 182 | ||
| 183 | updateDimensions(); | |
| 184 | ||
| 185 | lastFrameTime = performance.now(); | |
| 186 | animationFrameId = requestAnimationFrame(animate); | |
| 187 | }; | |
| 188 | ||
| 189 | init(); | |
| 190 | ||
| 191 | return () => { | |
| 192 | globalThis.removeEventListener("resize", updateDimensions); | |
| 193 | document.removeEventListener("mousemove", handleMouseMove); | |
| 194 | document.removeEventListener("touchmove", handleTouchMove); | |
| 195 | cancelAnimationFrame(animationFrameId); | |
| 196 | }; | |
| 197 | }; |
src/file-viewer/scripts/canvas_2024.client.ts created+251| ... | ... | @@ -0,0 +1,251 @@ |
| 1 | // Vibe coded with AI | |
| 2 | (globalThis as any).canvas_2024 = function (canvas: HTMLCanvasElement) { | |
| 3 | const isStandalone = canvas.getAttribute("data-standalone") === "true"; | |
| 4 | if (isStandalone) { | |
| 5 | canvas.parentElement!.style.backgroundColor = "black"; | |
| 6 | } | |
| 7 | ||
| 8 | const gl = canvas.getContext("webgl", { | |
| 9 | alpha: true, | |
| 10 | premultipliedAlpha: false, | |
| 11 | }); | |
| 12 | if (!gl) { | |
| 13 | console.error("WebGL not supported"); | |
| 14 | return () => {}; | |
| 15 | } | |
| 16 | ||
| 17 | canvas.style.imageRendering = "pixelated"; | |
| 18 | canvas.style.opacity = isStandalone ? "0.3" : "0.15"; | |
| 19 | ||
| 20 | // Resize canvas to match display size | |
| 21 | const resize = () => { | |
| 22 | const displayWidth = Math.floor( | |
| 23 | (canvas.clientWidth || globalThis.innerWidth) / 3, | |
| 24 | ); | |
| 25 | const displayHeight = Math.floor( | |
| 26 | (canvas.clientHeight || globalThis.innerHeight) / 3, | |
| 27 | ); | |
| 28 | ||
| 29 | if (canvas.width !== displayWidth || canvas.height !== displayHeight) { | |
| 30 | canvas.width = displayWidth; | |
| 31 | canvas.height = displayHeight; | |
| 32 | gl.viewport(0, 0, canvas.width, canvas.height); | |
| 33 | } | |
| 34 | }; | |
| 35 | resize(); | |
| 36 | ||
| 37 | // Vertex shader (just passes coordinates) | |
| 38 | const vertexShaderSource = ` | |
| 39 | attribute vec2 a_position; | |
| 40 | void main() { | |
| 41 | gl_Position = vec4(a_position, 0.0, 1.0); | |
| 42 | } | |
| 43 | `; | |
| 44 | ||
| 45 | // Fragment shader creates random noise with higher opacity to ensure visibility | |
| 46 | const fragmentShaderSource = ` | |
| 47 | precision mediump float; | |
| 48 | uniform float u_time; | |
| 49 | ||
| 50 | float noise1(float seed1,float seed2){ | |
| 51 | return( | |
| 52 | fract(seed1+12.34567* | |
| 53 | fract(100.*(abs(seed1*0.91)+seed2+94.68)* | |
| 54 | fract((abs(seed2*0.41)+45.46)* | |
| 55 | fract((abs(seed2)+757.21)* | |
| 56 | fract(seed1*0.0171)))))) | |
| 57 | * 1.0038 - 0.00185; | |
| 58 | } | |
| 59 | ||
| 60 | float n(float seed1, float seed2, float seed3){ | |
| 61 | float buff1 = abs(seed1+100.81) + 1000.3; | |
| 62 | float buff2 = abs(seed2+100.45) + 1000.2; | |
| 63 | float buff3 = abs(noise1(seed1, seed2)+seed3) + 1000.1; | |
| 64 | buff1 = (buff3*fract(buff2*fract(buff1*fract(buff2*0.146)))); | |
| 65 | buff2 = (buff2*fract(buff2*fract(buff1+buff2*fract(buff3*0.52)))); | |
| 66 | buff1 = noise1(buff1, buff2); | |
| 67 | return(buff1); | |
| 68 | } | |
| 69 | ||
| 70 | void main() { | |
| 71 | float noise = n(gl_FragCoord.x, gl_FragCoord.y, u_time); | |
| 72 | ||
| 73 | gl_FragColor = vec4(1.0, 0.7, 0.7, 0.8*noise); | |
| 74 | } | |
| 75 | `; | |
| 76 | ||
| 77 | // Create and compile shaders | |
| 78 | const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); | |
| 79 | const fragmentShader = createShader( | |
| 80 | gl, | |
| 81 | gl.FRAGMENT_SHADER, | |
| 82 | fragmentShaderSource, | |
| 83 | ); | |
| 84 | ||
| 85 | // Check if shader creation failed | |
| 86 | if (!vertexShader || !fragmentShader) { | |
| 87 | console.error("Failed to create shaders"); | |
| 88 | return () => {}; | |
| 89 | } | |
| 90 | ||
| 91 | // Create program and link shaders | |
| 92 | const program = createProgram(gl, vertexShader, fragmentShader); | |
| 93 | ||
| 94 | // Check if program creation failed | |
| 95 | if (!program) { | |
| 96 | console.error("Failed to create program"); | |
| 97 | return () => {}; | |
| 98 | } | |
| 99 | ||
| 100 | // Get attribute and uniform locations | |
| 101 | const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); | |
| 102 | const timeUniformLocation = gl.getUniformLocation(program, "u_time"); | |
| 103 | ||
| 104 | // Create a position buffer for a rectangle covering the entire canvas | |
| 105 | const positionBuffer = gl.createBuffer(); | |
| 106 | if (!positionBuffer) { | |
| 107 | console.error("Failed to create position buffer"); | |
| 108 | return () => {}; | |
| 109 | } | |
| 110 | ||
| 111 | gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
| 112 | // Rectangle that covers the entire clip space | |
| 113 | const positions = [ | |
| 114 | -1.0, | |
| 115 | -1.0, // bottom left | |
| 116 | 1.0, | |
| 117 | -1.0, // bottom right | |
| 118 | -1.0, | |
| 119 | 1.0, // top left | |
| 120 | 1.0, | |
| 121 | 1.0, // top right | |
| 122 | ]; | |
| 123 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); | |
| 124 | ||
| 125 | // Set up blending | |
| 126 | gl.enable(gl.BLEND); | |
| 127 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); | |
| 128 | ||
| 129 | // Fixed 24 FPS timing | |
| 130 | const FPS = 24; | |
| 131 | const FRAME_TIME = 1000 / FPS; // ms per frame | |
| 132 | ||
| 133 | // Handle animation | |
| 134 | let animationTimerId: number; | |
| 135 | let startTime = Date.now(); | |
| 136 | let lastFrameTime = 0; | |
| 137 | ||
| 138 | const render = () => { | |
| 139 | // Get current time | |
| 140 | const currentTime = Date.now(); | |
| 141 | const deltaTime = currentTime - lastFrameTime; | |
| 142 | ||
| 143 | // Skip frame if it's too early (maintain 24 FPS) | |
| 144 | if (deltaTime < FRAME_TIME) { | |
| 145 | animationTimerId = globalThis.setTimeout(render, 0); // Check again ASAP but yield to browser | |
| 146 | return; | |
| 147 | } | |
| 148 | ||
| 149 | // Update last frame time, accounting for any drift | |
| 150 | lastFrameTime = currentTime - (deltaTime % FRAME_TIME); | |
| 151 | ||
| 152 | // Resize canvas if needed | |
| 153 | resize(); | |
| 154 | ||
| 155 | // Calculate elapsed time in seconds for animation | |
| 156 | const elapsedTime = (currentTime - startTime) / 1000; | |
| 157 | ||
| 158 | // Clear the canvas with transparent black | |
| 159 | gl.clearColor(0, 0, 0, 0); | |
| 160 | gl.clear(gl.COLOR_BUFFER_BIT); | |
| 161 | ||
| 162 | // Use our shader program | |
| 163 | gl.useProgram(program); | |
| 164 | ||
| 165 | // Set up the position attribute | |
| 166 | gl.enableVertexAttribArray(positionAttributeLocation); | |
| 167 | gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
| 168 | gl.vertexAttribPointer( | |
| 169 | positionAttributeLocation, | |
| 170 | 2, // 2 components per vertex | |
| 171 | gl.FLOAT, // data type | |
| 172 | false, // normalize | |
| 173 | 0, // stride (0 = compute from size and type) | |
| 174 | 0, // offset | |
| 175 | ); | |
| 176 | ||
| 177 | // Update time uniform for animation | |
| 178 | gl.uniform1f(timeUniformLocation, elapsedTime); | |
| 179 | ||
| 180 | // Draw the rectangle (2 triangles) | |
| 181 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
| 182 | ||
| 183 | // Schedule next frame (aiming for 24 FPS) | |
| 184 | const timeToNextFrame = Math.max( | |
| 185 | 0, | |
| 186 | FRAME_TIME - (Date.now() - currentTime), | |
| 187 | ); | |
| 188 | animationTimerId = globalThis.setTimeout(render, timeToNextFrame); | |
| 189 | }; | |
| 190 | ||
| 191 | // Helper function to create shaders | |
| 192 | function createShader( | |
| 193 | gl: WebGLRenderingContext, | |
| 194 | type: number, | |
| 195 | source: string, | |
| 196 | ): WebGLShader | null { | |
| 197 | const shader = gl.createShader(type); | |
| 198 | if (!shader) { | |
| 199 | console.error("Failed to create shader object"); | |
| 200 | return null; | |
| 201 | } | |
| 202 | ||
| 203 | gl.shaderSource(shader, source); | |
| 204 | gl.compileShader(shader); | |
| 205 | ||
| 206 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | |
| 207 | console.error("Shader compilation error:", gl.getShaderInfoLog(shader)); | |
| 208 | gl.deleteShader(shader); | |
| 209 | return null; | |
| 210 | } | |
| 211 | ||
| 212 | return shader; | |
| 213 | } | |
| 214 | ||
| 215 | // Helper function to create program and link shaders | |
| 216 | function createProgram( | |
| 217 | gl: WebGLRenderingContext, | |
| 218 | vertexShader: WebGLShader, | |
| 219 | fragmentShader: WebGLShader, | |
| 220 | ): WebGLProgram | null { | |
| 221 | const program = gl.createProgram(); | |
| 222 | if (!program) { | |
| 223 | console.error("Failed to create program object"); | |
| 224 | return null; | |
| 225 | } | |
| 226 | ||
| 227 | gl.attachShader(program, vertexShader); | |
| 228 | gl.attachShader(program, fragmentShader); | |
| 229 | gl.linkProgram(program); | |
| 230 | ||
| 231 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { | |
| 232 | console.error("Program linking error:", gl.getProgramInfoLog(program)); | |
| 233 | return null; | |
| 234 | } | |
| 235 | ||
| 236 | return program; | |
| 237 | } | |
| 238 | ||
| 239 | // Start the rendering with initial timestamp | |
| 240 | lastFrameTime = Date.now(); | |
| 241 | render(); | |
| 242 | ||
| 243 | // Return cleanup function | |
| 244 | return () => { | |
| 245 | clearTimeout(animationTimerId); | |
| 246 | if (program) gl.deleteProgram(program); | |
| 247 | if (vertexShader) gl.deleteShader(vertexShader); | |
| 248 | if (fragmentShader) gl.deleteShader(fragmentShader); | |
| 249 | if (positionBuffer) gl.deleteBuffer(positionBuffer); | |
| 250 | }; | |
| 251 | }; |
src/file-viewer/scripts/canvas_cotyledon.client.ts created+197| ... | ... | @@ -0,0 +1,197 @@ |
| 1 | // __esModule | |
| 2 | true | |
| 3 | ||
| 4 | // computeNode | |
| 5 | function computeNode(node) { | |
| 6 | switch (node.type) { | |
| 7 | case "StringLiteral": | |
| 8 | case "NumericLiteral": | |
| 9 | case "BooleanLiteral": | |
| 10 | return { value: node.value }; | |
| 11 | case "RegExpLiteral": | |
| 12 | return { value: new RegExp(node.pattern, node.flags) }; | |
| 13 | case "NullLiteral": | |
| 14 | return { value: null }; | |
| 15 | case "Identifier": | |
| 16 | switch (node.name) { | |
| 17 | case "undefined": | |
| 18 | return { value: undefined }; | |
| 19 | case "NaN": | |
| 20 | return { value: NaN }; | |
| 21 | case "Infinity": | |
| 22 | return { value: Infinity }; | |
| 23 | default: | |
| 24 | return; | |
| 25 | } | |
| 26 | case "BigIntLiteral": | |
| 27 | return { value: BigInt(node.value) }; | |
| 28 | case "ParenthesizedExpression": | |
| 29 | return computeNode(node.expression); | |
| 30 | case "BinaryExpression":{ | |
| 31 | const left = computeNode(node.left); | |
| 32 | if (!left) return; | |
| 33 | const right = computeNode(node.right); | |
| 34 | if (!right) return; | |
| 35 | switch (node.operator) { | |
| 36 | case "+": | |
| 37 | return { value: left.value + right.value }; | |
| 38 | case "-": | |
| 39 | return { value: left.value - right.value }; | |
| 40 | case "*": | |
| 41 | return { value: left.value * right.value }; | |
| 42 | case "/": | |
| 43 | return { value: left.value / right.value }; | |
| 44 | case "%": | |
| 45 | return { value: left.value % right.value }; | |
| 46 | case "**": | |
| 47 | return { value: left.value ** right.value }; | |
| 48 | case "|": | |
| 49 | return { value: left.value | right.value }; | |
| 50 | case "&": | |
| 51 | return { value: left.value & right.value }; | |
| 52 | case "^": | |
| 53 | return { value: left.value ^ right.value }; | |
| 54 | case "<<": | |
| 55 | return { value: left.value << right.value }; | |
| 56 | case ">>": | |
| 57 | return { value: left.value >> right.value }; | |
| 58 | case ">>>": | |
| 59 | return { value: left.value >>> right.value }; | |
| 60 | case "==": | |
| 61 | return { value: left.value == right.value }; | |
| 62 | case "!=": | |
| 63 | return { value: left.value != right.value }; | |
| 64 | case "===": | |
| 65 | return { value: left.value === right.value }; | |
| 66 | case "!==": | |
| 67 | return { value: left.value !== right.value }; | |
| 68 | case "<": | |
| 69 | return { value: left.value < right.value }; | |
| 70 | case "<=": | |
| 71 | return { value: left.value <= right.value }; | |
| 72 | case ">": | |
| 73 | return { value: left.value > right.value }; | |
| 74 | case ">=": | |
| 75 | return { value: left.value >= right.value }; | |
| 76 | default: | |
| 77 | return; | |
| 78 | } | |
| 79 | } | |
| 80 | case "UnaryExpression":{ | |
| 81 | const arg = computeNode(node.argument); | |
| 82 | if (!arg) return; | |
| 83 | switch (node.operator) { | |
| 84 | case "+": | |
| 85 | return { value: +arg.value }; | |
| 86 | case "-": | |
| 87 | return { value: -arg.value }; | |
| 88 | case "~": | |
| 89 | return { value: ~arg.value }; | |
| 90 | case "!": | |
| 91 | return { value: !arg.value }; | |
| 92 | case "typeof": | |
| 93 | return { value: typeof arg.value }; | |
| 94 | case "void": | |
| 95 | return { value: void arg.value }; | |
| 96 | default: | |
| 97 | return; | |
| 98 | } | |
| 99 | } | |
| 100 | case "LogicalExpression":{ | |
| 101 | const left = computeNode(node.left); | |
| 102 | if (!left) return; | |
| 103 | const right = computeNode(node.right); | |
| 104 | if (!right) return; | |
| 105 | switch (node.operator) { | |
| 106 | case "&&": | |
| 107 | return { value: left.value && right.value }; | |
| 108 | case "||": | |
| 109 | return { value: left.value || right.value }; | |
| 110 | case "??": | |
| 111 | return { value: left.value ?? right.value }; | |
| 112 | default: | |
| 113 | return; | |
| 114 | } | |
| 115 | } | |
| 116 | case "ConditionalExpression":{ | |
| 117 | const test = computeNode(node.test); | |
| 118 | if (!test) return; | |
| 119 | const consequent = computeNode(node.consequent); | |
| 120 | if (!consequent) return; | |
| 121 | const alternate = computeNode(node.alternate); | |
| 122 | if (!alternate) return; | |
| 123 | return { value: test.value ? consequent.value : alternate.value }; | |
| 124 | } | |
| 125 | case "TemplateLiteral":{ | |
| 126 | let value = node.quasis[0].value.cooked; | |
| 127 | for (let i = 0; i < node.expressions.length; i++) { | |
| 128 | const expr = computeNode(node.expressions[i]); | |
| 129 | if (!expr) return; | |
| 130 | value += expr.value + node.quasis[i + 1].value.cooked; | |
| 131 | } | |
| 132 | return { value }; | |
| 133 | } | |
| 134 | case "ObjectExpression":{ | |
| 135 | const value = {}; | |
| 136 | for (const prop of node.properties) { | |
| 137 | if (prop.decorators) return; | |
| 138 | switch (prop.type) { | |
| 139 | case "ObjectProperty":{ | |
| 140 | let key; | |
| 141 | if (prop.computed) { | |
| 142 | const keyNode = computeNode(prop.key); | |
| 143 | if (!keyNode) return; | |
| 144 | key = keyNode.value + ""; | |
| 145 | } else { | |
| 146 | switch (prop.key.type) { | |
| 147 | case "Identifier": | |
| 148 | key = prop.key.name; | |
| 149 | break; | |
| 150 | case "StringLiteral": | |
| 151 | key = prop.key.value; | |
| 152 | break; | |
| 153 | default: | |
| 154 | return; | |
| 155 | } | |
| 156 | } | |
| 157 | ||
| 158 | const propValue = computeNode(prop.value); | |
| 159 | if (!propValue) return; | |
| 160 | value[key] = propValue.value; | |
| 161 | break; | |
| 162 | } | |
| 163 | case "SpreadElement":{ | |
| 164 | const arg = computeNode(prop.argument); | |
| 165 | if (!arg) return; | |
| 166 | Object.assign(value, arg.value); | |
| 167 | break; | |
| 168 | } | |
| 169 | } | |
| 170 | } | |
| 171 | ||
| 172 | return { value }; | |
| 173 | } | |
| 174 | case "ArrayExpression":{ | |
| 175 | const value = []; | |
| 176 | for (const elem of node.elements) { | |
| 177 | if (elem) { | |
| 178 | if (elem.type === "SpreadElement") { | |
| 179 | const arg = computeNode(elem.argument); | |
| 180 | if (typeof arg?.value?.[Symbol.iterator] !== "function") return; | |
| 181 | for (const item of arg.value) { | |
| 182 | value.push(item); | |
| 183 | } | |
| 184 | } else { | |
| 185 | const elemValue = computeNode(elem); | |
| 186 | if (!elemValue) return; | |
| 187 | value.push(elemValue.value); | |
| 188 | } | |
| 189 | } else { | |
| 190 | value.length++; | |
| 191 | } | |
| 192 | } | |
| 193 | ||
| 194 | return { value }; | |
| 195 | } | |
| 196 | } | |
| 197 | } |
src/file-viewer/server/.gitignore created+98| ... | ... | @@ -0,0 +1,98 @@ |
| 1 | // default | |
| 2 | class ImportBuilder { | |
| 3 | constructor(importedSource, scope, hub) { | |
| 4 | this._statements = []; | |
| 5 | this._resultName = null; | |
| 6 | this._importedSource = void 0; | |
| 7 | this._scope = scope; | |
| 8 | this._hub = hub; | |
| 9 | this._importedSource = importedSource; | |
| 10 | } | |
| 11 | done() { | |
| 12 | return { | |
| 13 | statements: this._statements, | |
| 14 | resultName: this._resultName | |
| 15 | }; | |
| 16 | } | |
| 17 | import() { | |
| 18 | this._statements.push(importDeclaration([], stringLiteral(this._importedSource))); | |
| 19 | return this; | |
| 20 | } | |
| 21 | require() { | |
| 22 | this._statements.push(expressionStatement(callExpression(identifier("require"), [stringLiteral(this._importedSource)]))); | |
| 23 | return this; | |
| 24 | } | |
| 25 | namespace(name = "namespace") { | |
| 26 | const local = this._scope.generateUidIdentifier(name); | |
| 27 | const statement = this._statements[this._statements.length - 1]; | |
| 28 | _assert(statement.type === "ImportDeclaration"); | |
| 29 | _assert(statement.specifiers.length === 0); | |
| 30 | statement.specifiers = [importNamespaceSpecifier(local)]; | |
| 31 | this._resultName = cloneNode(local); | |
| 32 | return this; | |
| 33 | } | |
| 34 | default(name) { | |
| 35 | const id = this._scope.generateUidIdentifier(name); | |
| 36 | const statement = this._statements[this._statements.length - 1]; | |
| 37 | _assert(statement.type === "ImportDeclaration"); | |
| 38 | _assert(statement.specifiers.length === 0); | |
| 39 | statement.specifiers = [importDefaultSpecifier(id)]; | |
| 40 | this._resultName = cloneNode(id); | |
| 41 | return this; | |
| 42 | } | |
| 43 | named(name, importName) { | |
| 44 | if (importName === "default") return this.default(name); | |
| 45 | const id = this._scope.generateUidIdentifier(name); | |
| 46 | const statement = this._statements[this._statements.length - 1]; | |
| 47 | _assert(statement.type === "ImportDeclaration"); | |
| 48 | _assert(statement.specifiers.length === 0); | |
| 49 | statement.specifiers = [importSpecifier(id, identifier(importName))]; | |
| 50 | this._resultName = cloneNode(id); | |
| 51 | return this; | |
| 52 | } | |
| 53 | var(name) { | |
| 54 | const id = this._scope.generateUidIdentifier(name); | |
| 55 | let statement = this._statements[this._statements.length - 1]; | |
| 56 | if (statement.type !== "ExpressionStatement") { | |
| 57 | _assert(this._resultName); | |
| 58 | statement = expressionStatement(this._resultName); | |
| 59 | this._statements.push(statement); | |
| 60 | } | |
| 61 | this._statements[this._statements.length - 1] = variableDeclaration("var", [variableDeclarator(id, statement.expression)]); | |
| 62 | this._resultName = cloneNode(id); | |
| 63 | return this; | |
| 64 | } | |
| 65 | defaultInterop() { | |
| 66 | return this._interop(this._hub.addHelper("interopRequireDefault")); | |
| 67 | } | |
| 68 | wildcardInterop() { | |
| 69 | return this._interop(this._hub.addHelper("interopRequireWildcard")); | |
| 70 | } | |
| 71 | _interop(callee) { | |
| 72 | const statement = this._statements[this._statements.length - 1]; | |
| 73 | if (statement.type === "ExpressionStatement") { | |
| 74 | statement.expression = callExpression(callee, [statement.expression]); | |
| 75 | } else if (statement.type === "VariableDeclaration") { | |
| 76 | _assert(statement.declarations.length === 1); | |
| 77 | statement.declarations[0].init = callExpression(callee, [statement.declarations[0].init]); | |
| 78 | } else { | |
| 79 | _assert.fail("Unexpected type."); | |
| 80 | } | |
| 81 | return this; | |
| 82 | } | |
| 83 | prop(name) { | |
| 84 | const statement = this._statements[this._statements.length - 1]; | |
| 85 | if (statement.type === "ExpressionStatement") { | |
| 86 | statement.expression = memberExpression(statement.expression, identifier(name)); | |
| 87 | } else if (statement.type === "VariableDeclaration") { | |
| 88 | _assert(statement.declarations.length === 1); | |
| 89 | statement.declarations[0].init = memberExpression(statement.declarations[0].init, identifier(name)); | |
| 90 | } else { | |
| 91 | _assert.fail("Unexpected type:" + statement.type); | |
| 92 | } | |
| 93 | return this; | |
| 94 | } | |
| 95 | read(name) { | |
| 96 | this._resultName = memberExpression(this._resultName, identifier(name)); | |
| 97 | } | |
| 98 | } |
src/file-viewer/server/CHANGES.md created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | // default | |
| 2 | function isModule(path) { | |
| 3 | return path.node.sourceType === "module"; | |
| 4 | } |
src/file-viewer/server/CLAUDE.md created+12| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | // default | |
| 2 | function rewriteThis(programPath) { | |
| 3 | if (!rewriteThisVisitor) { | |
| 4 | rewriteThisVisitor = _traverse.visitors.environmentVisitor({ | |
| 5 | ThisExpression(path) { | |
| 6 | path.replaceWith(_core.types.unaryExpression("void", _core.types.numericLiteral(0), true)); | |
| 7 | } | |
| 8 | }); | |
| 9 | rewriteThisVisitor.noScope = true; | |
| 10 | } | |
| 11 | (0, _traverse.default)(programPath.node, rewriteThisVisitor); | |
| 12 | } |
src/file-viewer/server/Cargo.lock created+61| ... | ... | @@ -0,0 +1,61 @@ |
| 1 | // default | |
| 2 | function rewriteLiveReferences(programPath, metadata, wrapReference) { | |
| 3 | const imported = new Map(); | |
| 4 | const exported = new Map(); | |
| 5 | const requeueInParent = path => { | |
| 6 | programPath.requeue(path); | |
| 7 | }; | |
| 8 | for (const [source, data] of metadata.source) { | |
| 9 | for (const [localName, importName] of data.imports) { | |
| 10 | imported.set(localName, [source, importName, null]); | |
| 11 | } | |
| 12 | for (const localName of data.importsNamespace) { | |
| 13 | imported.set(localName, [source, null, localName]); | |
| 14 | } | |
| 15 | } | |
| 16 | for (const [local, data] of metadata.local) { | |
| 17 | let exportMeta = exported.get(local); | |
| 18 | if (!exportMeta) { | |
| 19 | exportMeta = []; | |
| 20 | exported.set(local, exportMeta); | |
| 21 | } | |
| 22 | exportMeta.push(...data.names); | |
| 23 | } | |
| 24 | const rewriteBindingInitVisitorState = { | |
| 25 | metadata, | |
| 26 | requeueInParent, | |
| 27 | scope: programPath.scope, | |
| 28 | exported | |
| 29 | }; | |
| 30 | programPath.traverse(rewriteBindingInitVisitor, rewriteBindingInitVisitorState); | |
| 31 | const rewriteReferencesVisitorState = { | |
| 32 | seen: new WeakSet(), | |
| 33 | metadata, | |
| 34 | requeueInParent, | |
| 35 | scope: programPath.scope, | |
| 36 | imported, | |
| 37 | exported, | |
| 38 | buildImportReference([source, importName, localName], identNode) { | |
| 39 | const meta = metadata.source.get(source); | |
| 40 | meta.referenced = true; | |
| 41 | if (localName) { | |
| 42 | if (meta.wrap) { | |
| 43 | var _wrapReference; | |
| 44 | identNode = (_wrapReference = wrapReference(identNode, meta.wrap)) != null ? _wrapReference : identNode; | |
| 45 | } | |
| 46 | return identNode; | |
| 47 | } | |
| 48 | let namespace = _core.types.identifier(meta.name); | |
| 49 | if (meta.wrap) { | |
| 50 | var _wrapReference2; | |
| 51 | namespace = (_wrapReference2 = wrapReference(namespace, meta.wrap)) != null ? _wrapReference2 : namespace; | |
| 52 | } | |
| 53 | if (importName === "default" && meta.interop === "node-default") { | |
| 54 | return namespace; | |
| 55 | } | |
| 56 | const computed = metadata.stringSpecifiers.has(importName); | |
| 57 | return _core.types.memberExpression(namespace, computed ? _core.types.stringLiteral(importName) : _core.types.identifier(importName), computed); | |
| 58 | } | |
| 59 | }; | |
| 60 | programPath.traverse(rewriteReferencesVisitor, rewriteReferencesVisitorState); | |
| 61 | } |
src/file-viewer/server/Cargo.toml created+69| ... | ... | @@ -0,0 +1,69 @@ |
| 1 | // default | |
| 2 | function normalizeModuleAndLoadMetadata(programPath, exportName, { | |
| 3 | importInterop, | |
| 4 | initializeReexports = false, | |
| 5 | getWrapperPayload, | |
| 6 | esNamespaceOnly = false, | |
| 7 | filename | |
| 8 | }) { | |
| 9 | if (!exportName) { | |
| 10 | exportName = programPath.scope.generateUidIdentifier("exports").name; | |
| 11 | } | |
| 12 | const stringSpecifiers = new Set(); | |
| 13 | nameAnonymousExports(programPath); | |
| 14 | const { | |
| 15 | local, | |
| 16 | sources, | |
| 17 | hasExports | |
| 18 | } = getModuleMetadata(programPath, { | |
| 19 | initializeReexports, | |
| 20 | getWrapperPayload | |
| 21 | }, stringSpecifiers); | |
| 22 | removeImportExportDeclarations(programPath); | |
| 23 | for (const [source, metadata] of sources) { | |
| 24 | const { | |
| 25 | importsNamespace, | |
| 26 | imports | |
| 27 | } = metadata; | |
| 28 | if (importsNamespace.size > 0 && imports.size === 0) { | |
| 29 | const [nameOfnamespace] = importsNamespace; | |
| 30 | metadata.name = nameOfnamespace; | |
| 31 | } | |
| 32 | const resolvedInterop = resolveImportInterop(importInterop, source, filename); | |
| 33 | if (resolvedInterop === "none") { | |
| 34 | metadata.interop = "none"; | |
| 35 | } else if (resolvedInterop === "node" && metadata.interop === "namespace") { | |
| 36 | metadata.interop = "node-namespace"; | |
| 37 | } else if (resolvedInterop === "node" && metadata.interop === "default") { | |
| 38 | metadata.interop = "node-default"; | |
| 39 | } else if (esNamespaceOnly && metadata.interop === "namespace") { | |
| 40 | metadata.interop = "default"; | |
| 41 | } | |
| 42 | } | |
| 43 | return { | |
| 44 | exportName, | |
| 45 | exportNameListName: null, | |
| 46 | hasExports, | |
| 47 | local, | |
| 48 | source: sources, | |
| 49 | stringSpecifiers | |
| 50 | }; | |
| 51 | } | |
| 52 | ||
| 53 | // hasExports | |
| 54 | function hasExports(metadata) { | |
| 55 | return metadata.hasExports; | |
| 56 | } | |
| 57 | ||
| 58 | // isSideEffectImport | |
| 59 | function isSideEffectImport(source) { | |
| 60 | return source.imports.size === 0 && source.importsNamespace.size === 0 && source.reexports.size === 0 && source.reexportNamespace.size === 0 && !source.reexportAll; | |
| 61 | } | |
| 62 | ||
| 63 | // validateImportInteropOption | |
| 64 | function validateImportInteropOption(importInterop) { | |
| 65 | if (typeof importInterop !== "function" && importInterop !== "none" && importInterop !== "babel" && importInterop !== "node") { | |
| 66 | throw new Error(`.importInterop must be one of "none", "babel", "node", or a function returning one of those values (received ${importInterop}).`); | |
| 67 | } | |
| 68 | return importInterop; | |
| 69 | } |
src/file-viewer/server/DOCKER.md created+23| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | // toGetWrapperPayload | |
| 2 | function toGetWrapperPayload(lazy) { | |
| 3 | return (source, metadata) => { | |
| 4 | if (lazy === false) return null; | |
| 5 | if ((0, _normalizeAndLoadMetadata.isSideEffectImport)(metadata) || metadata.reexportAll) return null; | |
| 6 | if (lazy === true) { | |
| 7 | return source.includes(".") ? null : "lazy"; | |
| 8 | } | |
| 9 | if (Array.isArray(lazy)) { | |
| 10 | return !lazy.includes(source) ? null : "lazy"; | |
| 11 | } | |
| 12 | if (typeof lazy === "function") { | |
| 13 | return lazy(source) ? "lazy" : null; | |
| 14 | } | |
| 15 | throw new Error(`.lazy must be a boolean, string array, or function`); | |
| 16 | }; | |
| 17 | } | |
| 18 | ||
| 19 | // wrapReference | |
| 20 | function wrapReference(ref, payload) { | |
| 21 | if (payload === "lazy") return _core.types.callExpression(ref, []); | |
| 22 | return null; | |
| 23 | } |
src/file-viewer/server/Dockerfile created+40| ... | ... | @@ -0,0 +1,40 @@ |
| 1 | // buildDynamicImport | |
| 2 | function buildDynamicImport(node, deferToThen, wrapWithPromise, builder) { | |
| 3 | const specifier = _core.types.isCallExpression(node) ? node.arguments[0] : node.source; | |
| 4 | if (_core.types.isStringLiteral(specifier) || _core.types.isTemplateLiteral(specifier) && specifier.quasis.length === 0) { | |
| 5 | if (deferToThen) { | |
| 6 | return _core.template.expression.ast` | |
| 7 | Promise.resolve().then(() => ${builder(specifier)}) | |
| 8 | `; | |
| 9 | } else return builder(specifier); | |
| 10 | } | |
| 11 | const specifierToString = _core.types.isTemplateLiteral(specifier) ? _core.types.identifier("specifier") : _core.types.templateLiteral([_core.types.templateElement({ | |
| 12 | raw: "" | |
| 13 | }), _core.types.templateElement({ | |
| 14 | raw: "" | |
| 15 | })], [_core.types.identifier("specifier")]); | |
| 16 | if (deferToThen) { | |
| 17 | return _core.template.expression.ast` | |
| 18 | (specifier => | |
| 19 | new Promise(r => r(${specifierToString})) | |
| 20 | .then(s => ${builder(_core.types.identifier("s"))}) | |
| 21 | )(${specifier}) | |
| 22 | `; | |
| 23 | } else if (wrapWithPromise) { | |
| 24 | return _core.template.expression.ast` | |
| 25 | (specifier => | |
| 26 | new Promise(r => r(${builder(specifierToString)})) | |
| 27 | )(${specifier}) | |
| 28 | `; | |
| 29 | } else { | |
| 30 | return _core.template.expression.ast` | |
| 31 | (specifier => ${builder(specifierToString)})(${specifier}) | |
| 32 | `; | |
| 33 | } | |
| 34 | } | |
| 35 | ||
| 36 | // getDynamicImportSource | |
| 37 | function getDynamicImportSource(node) { | |
| 38 | const [source] = node.arguments; | |
| 39 | return _core.types.isStringLiteral(source) || _core.types.isTemplateLiteral(source) ? source : _core.template.expression.ast`\`\${${source}}\``; | |
| 40 | } |
src/file-viewer/server/README.md created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | // default | |
| 2 | function getModuleName(rootOpts, pluginOpts) { | |
| 3 | var _pluginOpts$moduleId, _pluginOpts$moduleIds, _pluginOpts$getModule, _pluginOpts$moduleRoo; | |
| 4 | return originalGetModuleName(rootOpts, { | |
| 5 | moduleId: (_pluginOpts$moduleId = pluginOpts.moduleId) != null ? _pluginOpts$moduleId : rootOpts.moduleId, | |
| 6 | moduleIds: (_pluginOpts$moduleIds = pluginOpts.moduleIds) != null ? _pluginOpts$moduleIds : rootOpts.moduleIds, | |
| 7 | getModuleId: (_pluginOpts$getModule = pluginOpts.getModuleId) != null ? _pluginOpts$getModule : rootOpts.getModuleId, | |
| 8 | moduleRoot: (_pluginOpts$moduleRoo = pluginOpts.moduleRoot) != null ? _pluginOpts$moduleRoo : rootOpts.moduleRoot | |
| 9 | }); | |
| 10 | } |
src/file-viewer/server/config.docker.toml created+5| ... | ... | @@ -0,0 +1,5 @@ |
| 1 | // transformDynamicImport | |
| 2 | function transformDynamicImport(path, noInterop, file) { | |
| 3 | const buildRequire = noInterop ? requireNoInterop : requireInterop; | |
| 4 | path.replaceWith((0, _helperModuleTransforms.buildDynamicImport)(path.node, true, false, specifier => buildRequire(specifier, file))); | |
| 5 | } |
src/file-viewer/server/config.example.toml created+34| ... | ... | @@ -0,0 +1,34 @@ |
| 1 | // lazyImportsHook | |
| 2 | lazy => ({ | |
| 3 | name: `${"@babel/plugin-transform-modules-commonjs"}/lazy`, | |
| 4 | version: "7.27.1", | |
| 5 | getWrapperPayload(source, metadata) { | |
| 6 | if ((0, _helperModuleTransforms.isSideEffectImport)(metadata) || metadata.reexportAll) { | |
| 7 | return null; | |
| 8 | } | |
| 9 | if (lazy === true) { | |
| 10 | return source.includes(".") ? null : "lazy/function"; | |
| 11 | } | |
| 12 | if (Array.isArray(lazy)) { | |
| 13 | return !lazy.includes(source) ? null : "lazy/function"; | |
| 14 | } | |
| 15 | if (typeof lazy === "function") { | |
| 16 | return lazy(source) ? "lazy/function" : null; | |
| 17 | } | |
| 18 | }, | |
| 19 | buildRequireWrapper(name, init, payload, referenced) { | |
| 20 | if (payload === "lazy/function") { | |
| 21 | if (!referenced) return false; | |
| 22 | return _core.template.statement.ast` | |
| 23 | function ${name}() { | |
| 24 | const data = ${init}; | |
| 25 | ${name} = function(){ return data; }; | |
| 26 | return data; | |
| 27 | } | |
| 28 | `; | |
| 29 | } | |
| 30 | }, | |
| 31 | wrapReference(ref, payload) { | |
| 32 | if (payload === "lazy/function") return _core.types.callExpression(ref, []); | |
| 33 | } | |
| 34 | }) |
src/file-viewer/server/config.test.toml created+22| ... | ... | @@ -0,0 +1,22 @@ |
| 1 | // defineCommonJSHook | |
| 2 | function defineCommonJSHook(file, hook) { | |
| 3 | let hooks = file.get(commonJSHooksKey); | |
| 4 | if (!hooks) file.set(commonJSHooksKey, hooks = []); | |
| 5 | hooks.push(hook); | |
| 6 | } | |
| 7 | ||
| 8 | // makeInvokers | |
| 9 | function makeInvokers(file) { | |
| 10 | const hooks = file.get(commonJSHooksKey); | |
| 11 | return { | |
| 12 | getWrapperPayload(...args) { | |
| 13 | return findMap(hooks, hook => hook.getWrapperPayload == null ? void 0 : hook.getWrapperPayload(...args)); | |
| 14 | }, | |
| 15 | wrapReference(...args) { | |
| 16 | return findMap(hooks, hook => hook.wrapReference == null ? void 0 : hook.wrapReference(...args)); | |
| 17 | }, | |
| 18 | buildRequireWrapper(...args) { | |
| 19 | return findMap(hooks, hook => hook.buildRequireWrapper == null ? void 0 : hook.buildRequireWrapper(...args)); | |
| 20 | } | |
| 21 | }; | |
| 22 | } |
src/file-viewer/server/docker-compose.yml created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | // default | |
| 2 | (api, options, dirname) => { | |
| 3 | let clonedApi; | |
| 4 | for (const name of Object.keys(apiPolyfills)) { | |
| 5 | if (api[name]) continue; | |
| 6 | clonedApi != null ? clonedApi : clonedApi = copyApiObject(api); | |
| 7 | clonedApi[name] = apiPolyfills[name](clonedApi); | |
| 8 | } | |
| 9 | return builder(clonedApi != null ? clonedApi : api, options || {}, dirname); | |
| 10 | } |
src/file-viewer/server/docker-compose.yml.bak created+298| ... | ... | @@ -0,0 +1,298 @@ |
| 1 | // FEATURES | |
| 2 | [object Object] | |
| 3 | ||
| 4 | // buildCheckInRHS | |
| 5 | function buildCheckInRHS(rhs, file, inRHSIsObject) { | |
| 6 | if (inRHSIsObject || !(file.availableHelper != null && file.availableHelper("checkInRHS"))) return rhs; | |
| 7 | return _core.types.callExpression(file.addHelper("checkInRHS"), [rhs]); | |
| 8 | } | |
| 9 | ||
| 10 | // createClassFeaturePlugin | |
| 11 | function createClassFeaturePlugin({ | |
| 12 | name, | |
| 13 | feature, | |
| 14 | loose, | |
| 15 | manipulateOptions, | |
| 16 | api, | |
| 17 | inherits, | |
| 18 | decoratorVersion | |
| 19 | }) { | |
| 20 | var _api$assumption; | |
| 21 | if (feature & _features.FEATURES.decorators) { | |
| 22 | { | |
| 23 | if (decoratorVersion === "2023-11" || decoratorVersion === "2023-05" || decoratorVersion === "2023-01" || decoratorVersion === "2022-03" || decoratorVersion === "2021-12") { | |
| 24 | return (0, _decorators.default)(api, { | |
| 25 | loose | |
| 26 | }, decoratorVersion, inherits); | |
| 27 | } | |
| 28 | } | |
| 29 | } | |
| 30 | { | |
| 31 | api != null ? api : api = { | |
| 32 | assumption: () => void 0 | |
| 33 | }; | |
| 34 | } | |
| 35 | const setPublicClassFields = api.assumption("setPublicClassFields"); | |
| 36 | const privateFieldsAsSymbols = api.assumption("privateFieldsAsSymbols"); | |
| 37 | const privateFieldsAsProperties = api.assumption("privateFieldsAsProperties"); | |
| 38 | const noUninitializedPrivateFieldAccess = (_api$assumption = api.assumption("noUninitializedPrivateFieldAccess")) != null ? _api$assumption : false; | |
| 39 | const constantSuper = api.assumption("constantSuper"); | |
| 40 | const noDocumentAll = api.assumption("noDocumentAll"); | |
| 41 | if (privateFieldsAsProperties && privateFieldsAsSymbols) { | |
| 42 | throw new Error(`Cannot enable both the "privateFieldsAsProperties" and ` + `"privateFieldsAsSymbols" assumptions as the same time.`); | |
| 43 | } | |
| 44 | const privateFieldsAsSymbolsOrProperties = privateFieldsAsProperties || privateFieldsAsSymbols; | |
| 45 | if (loose === true) { | |
| 46 | const explicit = []; | |
| 47 | if (setPublicClassFields !== undefined) { | |
| 48 | explicit.push(`"setPublicClassFields"`); | |
| 49 | } | |
| 50 | if (privateFieldsAsProperties !== undefined) { | |
| 51 | explicit.push(`"privateFieldsAsProperties"`); | |
| 52 | } | |
| 53 | if (privateFieldsAsSymbols !== undefined) { | |
| 54 | explicit.push(`"privateFieldsAsSymbols"`); | |
| 55 | } | |
| 56 | if (explicit.length !== 0) { | |
| 57 | console.warn(`[${name}]: You are using the "loose: true" option and you are` + ` explicitly setting a value for the ${explicit.join(" and ")}` + ` assumption${explicit.length > 1 ? "s" : ""}. The "loose" option` + ` can cause incompatibilities with the other class features` + ` plugins, so it's recommended that you replace it with the` + ` following top-level option:\n` + `\t"assumptions": {\n` + `\t\t"setPublicClassFields": true,\n` + `\t\t"privateFieldsAsSymbols": true\n` + `\t}`); | |
| 58 | } | |
| 59 | } | |
| 60 | return { | |
| 61 | name, | |
| 62 | manipulateOptions, | |
| 63 | inherits, | |
| 64 | pre(file) { | |
| 65 | (0, _features.enableFeature)(file, feature, loose); | |
| 66 | { | |
| 67 | if (typeof file.get(versionKey) === "number") { | |
| 68 | file.set(versionKey, "7.27.1"); | |
| 69 | return; | |
| 70 | } | |
| 71 | } | |
| 72 | if (!file.get(versionKey) || _semver.lt(file.get(versionKey), "7.27.1")) { | |
| 73 | file.set(versionKey, "7.27.1"); | |
| 74 | } | |
| 75 | }, | |
| 76 | visitor: { | |
| 77 | Class(path, { | |
| 78 | file | |
| 79 | }) { | |
| 80 | if (file.get(versionKey) !== "7.27.1") return; | |
| 81 | if (!(0, _features.shouldTransform)(path, file)) return; | |
| 82 | const pathIsClassDeclaration = path.isClassDeclaration(); | |
| 83 | if (pathIsClassDeclaration) (0, _typescript.assertFieldTransformed)(path); | |
| 84 | const loose = (0, _features.isLoose)(file, feature); | |
| 85 | let constructor; | |
| 86 | const isDecorated = (0, _decorators.hasDecorators)(path.node); | |
| 87 | const props = []; | |
| 88 | const elements = []; | |
| 89 | const computedPaths = []; | |
| 90 | const privateNames = new Set(); | |
| 91 | const body = path.get("body"); | |
| 92 | for (const path of body.get("body")) { | |
| 93 | if ((path.isClassProperty() || path.isClassMethod()) && path.node.computed) { | |
| 94 | computedPaths.push(path); | |
| 95 | } | |
| 96 | if (path.isPrivate()) { | |
| 97 | const { | |
| 98 | name | |
| 99 | } = path.node.key.id; | |
| 100 | const getName = `get ${name}`; | |
| 101 | const setName = `set ${name}`; | |
| 102 | if (path.isClassPrivateMethod()) { | |
| 103 | if (path.node.kind === "get") { | |
| 104 | if (privateNames.has(getName) || privateNames.has(name) && !privateNames.has(setName)) { | |
| 105 | throw path.buildCodeFrameError("Duplicate private field"); | |
| 106 | } | |
| 107 | privateNames.add(getName).add(name); | |
| 108 | } else if (path.node.kind === "set") { | |
| 109 | if (privateNames.has(setName) || privateNames.has(name) && !privateNames.has(getName)) { | |
| 110 | throw path.buildCodeFrameError("Duplicate private field"); | |
| 111 | } | |
| 112 | privateNames.add(setName).add(name); | |
| 113 | } | |
| 114 | } else { | |
| 115 | if (privateNames.has(name) && !privateNames.has(getName) && !privateNames.has(setName) || privateNames.has(name) && (privateNames.has(getName) || privateNames.has(setName))) { | |
| 116 | throw path.buildCodeFrameError("Duplicate private field"); | |
| 117 | } | |
| 118 | privateNames.add(name); | |
| 119 | } | |
| 120 | } | |
| 121 | if (path.isClassMethod({ | |
| 122 | kind: "constructor" | |
| 123 | })) { | |
| 124 | constructor = path; | |
| 125 | } else { | |
| 126 | elements.push(path); | |
| 127 | if (path.isProperty() || path.isPrivate() || path.isStaticBlock != null && path.isStaticBlock()) { | |
| 128 | props.push(path); | |
| 129 | } | |
| 130 | } | |
| 131 | } | |
| 132 | { | |
| 133 | if (!props.length && !isDecorated) return; | |
| 134 | } | |
| 135 | const innerBinding = path.node.id; | |
| 136 | let ref; | |
| 137 | if (!innerBinding || !pathIsClassDeclaration) { | |
| 138 | { | |
| 139 | var _path$ensureFunctionN; | |
| 140 | (_path$ensureFunctionN = path.ensureFunctionName) != null ? _path$ensureFunctionN : path.ensureFunctionName = require("@babel/traverse").NodePath.prototype.ensureFunctionName; | |
| 141 | } | |
| 142 | path.ensureFunctionName(false); | |
| 143 | ref = path.scope.generateUidIdentifier((innerBinding == null ? void 0 : innerBinding.name) || "Class"); | |
| 144 | } | |
| 145 | const classRefForDefine = ref != null ? ref : _core.types.cloneNode(innerBinding); | |
| 146 | const privateNamesMap = (0, _fields.buildPrivateNamesMap)(classRefForDefine.name, privateFieldsAsSymbolsOrProperties != null ? privateFieldsAsSymbolsOrProperties : loose, props, file); | |
| 147 | const privateNamesNodes = (0, _fields.buildPrivateNamesNodes)(privateNamesMap, privateFieldsAsProperties != null ? privateFieldsAsProperties : loose, privateFieldsAsSymbols != null ? privateFieldsAsSymbols : false, file); | |
| 148 | (0, _fields.transformPrivateNamesUsage)(classRefForDefine, path, privateNamesMap, { | |
| 149 | privateFieldsAsProperties: privateFieldsAsSymbolsOrProperties != null ? privateFieldsAsSymbolsOrProperties : loose, | |
| 150 | noUninitializedPrivateFieldAccess, | |
| 151 | noDocumentAll, | |
| 152 | innerBinding | |
| 153 | }, file); | |
| 154 | let keysNodes, staticNodes, instanceNodes, lastInstanceNodeReturnsThis, pureStaticNodes, classBindingNode, wrapClass; | |
| 155 | { | |
| 156 | if (isDecorated) { | |
| 157 | staticNodes = pureStaticNodes = keysNodes = []; | |
| 158 | ({ | |
| 159 | instanceNodes, | |
| 160 | wrapClass | |
| 161 | } = (0, _decorators2.buildDecoratedClass)(classRefForDefine, path, elements, file)); | |
| 162 | } else { | |
| 163 | keysNodes = (0, _misc.extractComputedKeys)(path, computedPaths, file); | |
| 164 | ({ | |
| 165 | staticNodes, | |
| 166 | pureStaticNodes, | |
| 167 | instanceNodes, | |
| 168 | lastInstanceNodeReturnsThis, | |
| 169 | classBindingNode, | |
| 170 | wrapClass | |
| 171 | } = (0, _fields.buildFieldsInitNodes)(ref, path.node.superClass, props, privateNamesMap, file, setPublicClassFields != null ? setPublicClassFields : loose, privateFieldsAsSymbolsOrProperties != null ? privateFieldsAsSymbolsOrProperties : loose, noUninitializedPrivateFieldAccess, constantSuper != null ? constantSuper : loose, innerBinding)); | |
| 172 | } | |
| 173 | } | |
| 174 | if (instanceNodes.length > 0) { | |
| 175 | (0, _misc.injectInitialization)(path, constructor, instanceNodes, (referenceVisitor, state) => { | |
| 176 | { | |
| 177 | if (isDecorated) return; | |
| 178 | } | |
| 179 | for (const prop of props) { | |
| 180 | if (_core.types.isStaticBlock != null && _core.types.isStaticBlock(prop.node) || prop.node.static) continue; | |
| 181 | prop.traverse(referenceVisitor, state); | |
| 182 | } | |
| 183 | }, lastInstanceNodeReturnsThis); | |
| 184 | } | |
| 185 | const wrappedPath = wrapClass(path); | |
| 186 | wrappedPath.insertBefore([...privateNamesNodes, ...keysNodes]); | |
| 187 | if (staticNodes.length > 0) { | |
| 188 | wrappedPath.insertAfter(staticNodes); | |
| 189 | } | |
| 190 | if (pureStaticNodes.length > 0) { | |
| 191 | wrappedPath.find(parent => parent.isStatement() || parent.isDeclaration()).insertAfter(pureStaticNodes); | |
| 192 | } | |
| 193 | if (classBindingNode != null && pathIsClassDeclaration) { | |
| 194 | wrappedPath.insertAfter(classBindingNode); | |
| 195 | } | |
| 196 | }, | |
| 197 | ExportDefaultDeclaration(path, { | |
| 198 | file | |
| 199 | }) { | |
| 200 | { | |
| 201 | if (file.get(versionKey) !== "7.27.1") return; | |
| 202 | const decl = path.get("declaration"); | |
| 203 | if (decl.isClassDeclaration() && (0, _decorators.hasDecorators)(decl.node)) { | |
| 204 | if (decl.node.id) { | |
| 205 | { | |
| 206 | var _path$splitExportDecl; | |
| 207 | (_path$splitExportDecl = path.splitExportDeclaration) != null ? _path$splitExportDecl : path.splitExportDeclaration = require("@babel/traverse").NodePath.prototype.splitExportDeclaration; | |
| 208 | } | |
| 209 | path.splitExportDeclaration(); | |
| 210 | } else { | |
| 211 | decl.node.type = "ClassExpression"; | |
| 212 | } | |
| 213 | } | |
| 214 | } | |
| 215 | } | |
| 216 | } | |
| 217 | }; | |
| 218 | } | |
| 219 | ||
| 220 | // enableFeature | |
| 221 | function enableFeature(file, feature, loose) { | |
| 222 | if (!hasFeature(file, feature) || canIgnoreLoose(file, feature)) { | |
| 223 | file.set(featuresKey, file.get(featuresKey) | feature); | |
| 224 | if (loose === "#__internal__@babel/preset-env__prefer-true-but-false-is-ok-if-it-prevents-an-error") { | |
| 225 | setLoose(file, feature, true); | |
| 226 | file.set(looseLowPriorityKey, file.get(looseLowPriorityKey) | feature); | |
| 227 | } else if (loose === "#__internal__@babel/preset-env__prefer-false-but-true-is-ok-if-it-prevents-an-error") { | |
| 228 | setLoose(file, feature, false); | |
| 229 | file.set(looseLowPriorityKey, file.get(looseLowPriorityKey) | feature); | |
| 230 | } else { | |
| 231 | setLoose(file, feature, loose); | |
| 232 | } | |
| 233 | } | |
| 234 | let resolvedLoose; | |
| 235 | for (const [mask, name] of featuresSameLoose) { | |
| 236 | if (!hasFeature(file, mask)) continue; | |
| 237 | { | |
| 238 | if (canIgnoreLoose(file, mask)) continue; | |
| 239 | } | |
| 240 | const loose = isLoose(file, mask); | |
| 241 | if (resolvedLoose === !loose) { | |
| 242 | throw new Error("'loose' mode configuration must be the same for @babel/plugin-transform-class-properties, " + "@babel/plugin-transform-private-methods and " + "@babel/plugin-transform-private-property-in-object (when they are enabled)." + "\n\n" + getBabelShowConfigForHint(file)); | |
| 243 | } else { | |
| 244 | resolvedLoose = loose; | |
| 245 | { | |
| 246 | var higherPriorityPluginName = name; | |
| 247 | } | |
| 248 | } | |
| 249 | } | |
| 250 | if (resolvedLoose !== undefined) { | |
| 251 | for (const [mask, name] of featuresSameLoose) { | |
| 252 | if (hasFeature(file, mask) && isLoose(file, mask) !== resolvedLoose) { | |
| 253 | setLoose(file, mask, resolvedLoose); | |
| 254 | console.warn(`Though the "loose" option was set to "${!resolvedLoose}" in your @babel/preset-env ` + `config, it will not be used for ${name} since the "loose" mode option was set to ` + `"${resolvedLoose}" for ${higherPriorityPluginName}.\nThe "loose" option must be the ` + `same for @babel/plugin-transform-class-properties, @babel/plugin-transform-private-methods ` + `and @babel/plugin-transform-private-property-in-object (when they are enabled): you can ` + `silence this warning by explicitly adding\n` + `\t["${name}", { "loose": ${resolvedLoose} }]\n` + `to the "plugins" section of your Babel config.` + "\n\n" + getBabelShowConfigForHint(file)); | |
| 255 | } | |
| 256 | } | |
| 257 | } | |
| 258 | } | |
| 259 | ||
| 260 | // injectInitialization | |
| 261 | function injectInitialization(path, constructor, nodes, renamer, lastReturnsThis) { | |
| 262 | if (!nodes.length) return; | |
| 263 | const isDerived = !!path.node.superClass; | |
| 264 | if (!constructor) { | |
| 265 | const newConstructor = _core.types.classMethod("constructor", _core.types.identifier("constructor"), [], _core.types.blockStatement([])); | |
| 266 | if (isDerived) { | |
| 267 | newConstructor.params = [_core.types.restElement(_core.types.identifier("args"))]; | |
| 268 | newConstructor.body.body.push(_core.template.statement.ast`super(...args)`); | |
| 269 | } | |
| 270 | [constructor] = path.get("body").unshiftContainer("body", newConstructor); | |
| 271 | } | |
| 272 | if (renamer) { | |
| 273 | renamer(referenceVisitor, { | |
| 274 | scope: constructor.scope | |
| 275 | }); | |
| 276 | } | |
| 277 | if (isDerived) { | |
| 278 | const bareSupers = []; | |
| 279 | constructor.traverse(findBareSupers, bareSupers); | |
| 280 | let isFirst = true; | |
| 281 | for (const bareSuper of bareSupers) { | |
| 282 | if (isFirst) { | |
| 283 | isFirst = false; | |
| 284 | } else { | |
| 285 | nodes = nodes.map(n => _core.types.cloneNode(n)); | |
| 286 | } | |
| 287 | if (!bareSuper.parentPath.isExpressionStatement()) { | |
| 288 | const allNodes = [bareSuper.node, ...nodes.map(n => _core.types.toExpression(n))]; | |
| 289 | if (!lastReturnsThis) allNodes.push(_core.types.thisExpression()); | |
| 290 | bareSuper.replaceWith(_core.types.sequenceExpression(allNodes)); | |
| 291 | } else { | |
| 292 | bareSuper.insertAfter(nodes); | |
| 293 | } | |
| 294 | } | |
| 295 | } else { | |
| 296 | constructor.get("body").unshiftContainer("body", nodes); | |
| 297 | } | |
| 298 | } |
src/file-viewer/server/generate_certs.sh created+323| ... | ... | @@ -0,0 +1,323 @@ |
| 1 | // buildCheckInRHS | |
| 2 | function buildCheckInRHS(rhs, file, inRHSIsObject) { | |
| 3 | if (inRHSIsObject || !(file.availableHelper != null && file.availableHelper("checkInRHS"))) return rhs; | |
| 4 | return _core.types.callExpression(file.addHelper("checkInRHS"), [rhs]); | |
| 5 | } | |
| 6 | ||
| 7 | // buildFieldsInitNodes | |
| 8 | function buildFieldsInitNodes(ref, superRef, props, privateNamesMap, file, setPublicClassFields, privateFieldsAsSymbolsOrProperties, noUninitializedPrivateFieldAccess, constantSuper, innerBindingRef) { | |
| 9 | let classRefFlags = 0; | |
| 10 | let injectSuperRef; | |
| 11 | const staticNodes = []; | |
| 12 | const instanceNodes = []; | |
| 13 | let lastInstanceNodeReturnsThis = false; | |
| 14 | const pureStaticNodes = []; | |
| 15 | let classBindingNode = null; | |
| 16 | const getSuperRef = _core.types.isIdentifier(superRef) ? () => superRef : () => { | |
| 17 | injectSuperRef != null ? injectSuperRef : injectSuperRef = props[0].scope.generateUidIdentifierBasedOnNode(superRef); | |
| 18 | return injectSuperRef; | |
| 19 | }; | |
| 20 | const classRefForInnerBinding = ref != null ? ref : props[0].scope.generateUidIdentifier((innerBindingRef == null ? void 0 : innerBindingRef.name) || "Class"); | |
| 21 | ref != null ? ref : ref = _core.types.cloneNode(innerBindingRef); | |
| 22 | for (const prop of props) { | |
| 23 | if (prop.isClassProperty()) { | |
| 24 | ts.assertFieldTransformed(prop); | |
| 25 | } | |
| 26 | const isStatic = !(_core.types.isStaticBlock != null && _core.types.isStaticBlock(prop.node)) && prop.node.static; | |
| 27 | const isInstance = !isStatic; | |
| 28 | const isPrivate = prop.isPrivate(); | |
| 29 | const isPublic = !isPrivate; | |
| 30 | const isField = prop.isProperty(); | |
| 31 | const isMethod = !isField; | |
| 32 | const isStaticBlock = prop.isStaticBlock == null ? void 0 : prop.isStaticBlock(); | |
| 33 | if (isStatic) classRefFlags |= 1; | |
| 34 | if (isStatic || isMethod && isPrivate || isStaticBlock) { | |
| 35 | new _helperReplaceSupers.default({ | |
| 36 | methodPath: prop, | |
| 37 | constantSuper, | |
| 38 | file: file, | |
| 39 | refToPreserve: innerBindingRef, | |
| 40 | getSuperRef, | |
| 41 | getObjectRef() { | |
| 42 | classRefFlags |= 2; | |
| 43 | if (isStatic || isStaticBlock) { | |
| 44 | return classRefForInnerBinding; | |
| 45 | } else { | |
| 46 | return _core.types.memberExpression(classRefForInnerBinding, _core.types.identifier("prototype")); | |
| 47 | } | |
| 48 | } | |
| 49 | }).replace(); | |
| 50 | const replaced = replaceThisContext(prop, classRefForInnerBinding, innerBindingRef); | |
| 51 | if (replaced) { | |
| 52 | classRefFlags |= 2; | |
| 53 | } | |
| 54 | } | |
| 55 | lastInstanceNodeReturnsThis = false; | |
| 56 | switch (true) { | |
| 57 | case isStaticBlock: | |
| 58 | { | |
| 59 | const blockBody = prop.node.body; | |
| 60 | if (blockBody.length === 1 && _core.types.isExpressionStatement(blockBody[0])) { | |
| 61 | staticNodes.push(inheritPropComments(blockBody[0], prop)); | |
| 62 | } else { | |
| 63 | staticNodes.push(_core.types.inheritsComments(_core.template.statement.ast`(() => { ${blockBody} })()`, prop.node)); | |
| 64 | } | |
| 65 | break; | |
| 66 | } | |
| 67 | case isStatic && isPrivate && isField && privateFieldsAsSymbolsOrProperties: | |
| 68 | staticNodes.push(buildPrivateFieldInitLoose(_core.types.cloneNode(ref), prop, privateNamesMap)); | |
| 69 | break; | |
| 70 | case isStatic && isPrivate && isField && !privateFieldsAsSymbolsOrProperties: | |
| 71 | if (!newHelpers(file)) { | |
| 72 | staticNodes.push(buildPrivateStaticFieldInitSpecOld(prop, privateNamesMap)); | |
| 73 | } else { | |
| 74 | staticNodes.push(buildPrivateStaticFieldInitSpec(prop, privateNamesMap, noUninitializedPrivateFieldAccess)); | |
| 75 | } | |
| 76 | break; | |
| 77 | case isStatic && isPublic && isField && setPublicClassFields: | |
| 78 | if (!isNameOrLength(prop.node)) { | |
| 79 | staticNodes.push(buildPublicFieldInitLoose(_core.types.cloneNode(ref), prop)); | |
| 80 | break; | |
| 81 | } | |
| 82 | case isStatic && isPublic && isField && !setPublicClassFields: | |
| 83 | staticNodes.push(buildPublicFieldInitSpec(_core.types.cloneNode(ref), prop, file)); | |
| 84 | break; | |
| 85 | case isInstance && isPrivate && isField && privateFieldsAsSymbolsOrProperties: | |
| 86 | instanceNodes.push(buildPrivateFieldInitLoose(_core.types.thisExpression(), prop, privateNamesMap)); | |
| 87 | break; | |
| 88 | case isInstance && isPrivate && isField && !privateFieldsAsSymbolsOrProperties: | |
| 89 | instanceNodes.push(buildPrivateInstanceFieldInitSpec(_core.types.thisExpression(), prop, privateNamesMap, file)); | |
| 90 | break; | |
| 91 | case isInstance && isPrivate && isMethod && privateFieldsAsSymbolsOrProperties: | |
| 92 | instanceNodes.unshift(buildPrivateMethodInitLoose(_core.types.thisExpression(), prop, privateNamesMap)); | |
| 93 | pureStaticNodes.push(buildPrivateMethodDeclaration(file, prop, privateNamesMap, privateFieldsAsSymbolsOrProperties)); | |
| 94 | break; | |
| 95 | case isInstance && isPrivate && isMethod && !privateFieldsAsSymbolsOrProperties: | |
| 96 | instanceNodes.unshift(buildPrivateInstanceMethodInitSpec(_core.types.thisExpression(), prop, privateNamesMap, file)); | |
| 97 | pureStaticNodes.push(buildPrivateMethodDeclaration(file, prop, privateNamesMap, privateFieldsAsSymbolsOrProperties)); | |
| 98 | break; | |
| 99 | case isStatic && isPrivate && isMethod && !privateFieldsAsSymbolsOrProperties: | |
| 100 | if (!newHelpers(file)) { | |
| 101 | staticNodes.unshift(buildPrivateStaticFieldInitSpecOld(prop, privateNamesMap)); | |
| 102 | } | |
| 103 | pureStaticNodes.push(buildPrivateMethodDeclaration(file, prop, privateNamesMap, privateFieldsAsSymbolsOrProperties)); | |
| 104 | break; | |
| 105 | case isStatic && isPrivate && isMethod && privateFieldsAsSymbolsOrProperties: | |
| 106 | staticNodes.unshift(buildPrivateStaticMethodInitLoose(_core.types.cloneNode(ref), prop, file, privateNamesMap)); | |
| 107 | pureStaticNodes.push(buildPrivateMethodDeclaration(file, prop, privateNamesMap, privateFieldsAsSymbolsOrProperties)); | |
| 108 | break; | |
| 109 | case isInstance && isPublic && isField && setPublicClassFields: | |
| 110 | instanceNodes.push(buildPublicFieldInitLoose(_core.types.thisExpression(), prop)); | |
| 111 | break; | |
| 112 | case isInstance && isPublic && isField && !setPublicClassFields: | |
| 113 | lastInstanceNodeReturnsThis = true; | |
| 114 | instanceNodes.push(buildPublicFieldInitSpec(_core.types.thisExpression(), prop, file)); | |
| 115 | break; | |
| 116 | default: | |
| 117 | throw new Error("Unreachable."); | |
| 118 | } | |
| 119 | } | |
| 120 | if (classRefFlags & 2 && innerBindingRef != null) { | |
| 121 | classBindingNode = _core.types.expressionStatement(_core.types.assignmentExpression("=", _core.types.cloneNode(classRefForInnerBinding), _core.types.cloneNode(innerBindingRef))); | |
| 122 | } | |
| 123 | return { | |
| 124 | staticNodes: staticNodes.filter(Boolean), | |
| 125 | instanceNodes: instanceNodes.filter(Boolean), | |
| 126 | lastInstanceNodeReturnsThis, | |
| 127 | pureStaticNodes: pureStaticNodes.filter(Boolean), | |
| 128 | classBindingNode, | |
| 129 | wrapClass(path) { | |
| 130 | for (const prop of props) { | |
| 131 | prop.node.leadingComments = null; | |
| 132 | prop.remove(); | |
| 133 | } | |
| 134 | if (injectSuperRef) { | |
| 135 | path.scope.push({ | |
| 136 | id: _core.types.cloneNode(injectSuperRef) | |
| 137 | }); | |
| 138 | path.set("superClass", _core.types.assignmentExpression("=", injectSuperRef, path.node.superClass)); | |
| 139 | } | |
| 140 | if (classRefFlags !== 0) { | |
| 141 | if (path.isClassExpression()) { | |
| 142 | path.scope.push({ | |
| 143 | id: ref | |
| 144 | }); | |
| 145 | path.replaceWith(_core.types.assignmentExpression("=", _core.types.cloneNode(ref), path.node)); | |
| 146 | } else { | |
| 147 | if (innerBindingRef == null) { | |
| 148 | path.node.id = ref; | |
| 149 | } | |
| 150 | if (classBindingNode != null) { | |
| 151 | path.scope.push({ | |
| 152 | id: classRefForInnerBinding | |
| 153 | }); | |
| 154 | } | |
| 155 | } | |
| 156 | } | |
| 157 | return path; | |
| 158 | } | |
| 159 | }; | |
| 160 | } | |
| 161 | ||
| 162 | // buildPrivateNamesMap | |
| 163 | function buildPrivateNamesMap(className, privateFieldsAsSymbolsOrProperties, props, file) { | |
| 164 | const privateNamesMap = new Map(); | |
| 165 | let classBrandId; | |
| 166 | for (const prop of props) { | |
| 167 | if (prop.isPrivate()) { | |
| 168 | const { | |
| 169 | name | |
| 170 | } = prop.node.key.id; | |
| 171 | let update = privateNamesMap.get(name); | |
| 172 | if (!update) { | |
| 173 | const isMethod = !prop.isProperty(); | |
| 174 | const isStatic = prop.node.static; | |
| 175 | let initAdded = false; | |
| 176 | let id; | |
| 177 | if (!privateFieldsAsSymbolsOrProperties && newHelpers(file) && isMethod && !isStatic) { | |
| 178 | initAdded = !!classBrandId; | |
| 179 | classBrandId != null ? classBrandId : classBrandId = prop.scope.generateUidIdentifier(`${className}_brand`); | |
| 180 | id = classBrandId; | |
| 181 | } else { | |
| 182 | id = prop.scope.generateUidIdentifier(name); | |
| 183 | } | |
| 184 | update = { | |
| 185 | id, | |
| 186 | static: isStatic, | |
| 187 | method: isMethod, | |
| 188 | initAdded | |
| 189 | }; | |
| 190 | privateNamesMap.set(name, update); | |
| 191 | } | |
| 192 | if (prop.isClassPrivateMethod()) { | |
| 193 | if (prop.node.kind === "get") { | |
| 194 | const { | |
| 195 | body | |
| 196 | } = prop.node.body; | |
| 197 | let $; | |
| 198 | if (body.length === 1 && _core.types.isReturnStatement($ = body[0]) && _core.types.isCallExpression($ = $.argument) && $.arguments.length === 1 && _core.types.isThisExpression($.arguments[0]) && _core.types.isIdentifier($ = $.callee)) { | |
| 199 | update.getId = _core.types.cloneNode($); | |
| 200 | update.getterDeclared = true; | |
| 201 | } else { | |
| 202 | update.getId = prop.scope.generateUidIdentifier(`get_${name}`); | |
| 203 | } | |
| 204 | } else if (prop.node.kind === "set") { | |
| 205 | const { | |
| 206 | params | |
| 207 | } = prop.node; | |
| 208 | const { | |
| 209 | body | |
| 210 | } = prop.node.body; | |
| 211 | let $; | |
| 212 | if (body.length === 1 && _core.types.isExpressionStatement($ = body[0]) && _core.types.isCallExpression($ = $.expression) && $.arguments.length === 2 && _core.types.isThisExpression($.arguments[0]) && _core.types.isIdentifier($.arguments[1], { | |
| 213 | name: params[0].name | |
| 214 | }) && _core.types.isIdentifier($ = $.callee)) { | |
| 215 | update.setId = _core.types.cloneNode($); | |
| 216 | update.setterDeclared = true; | |
| 217 | } else { | |
| 218 | update.setId = prop.scope.generateUidIdentifier(`set_${name}`); | |
| 219 | } | |
| 220 | } else if (prop.node.kind === "method") { | |
| 221 | update.methodId = prop.scope.generateUidIdentifier(name); | |
| 222 | } | |
| 223 | } | |
| 224 | privateNamesMap.set(name, update); | |
| 225 | } | |
| 226 | } | |
| 227 | return privateNamesMap; | |
| 228 | } | |
| 229 | ||
| 230 | // buildPrivateNamesNodes | |
| 231 | function buildPrivateNamesNodes(privateNamesMap, privateFieldsAsProperties, privateFieldsAsSymbols, state) { | |
| 232 | const initNodes = []; | |
| 233 | const injectedIds = new Set(); | |
| 234 | for (const [name, value] of privateNamesMap) { | |
| 235 | const { | |
| 236 | static: isStatic, | |
| 237 | method: isMethod, | |
| 238 | getId, | |
| 239 | setId | |
| 240 | } = value; | |
| 241 | const isGetterOrSetter = getId || setId; | |
| 242 | const id = _core.types.cloneNode(value.id); | |
| 243 | let init; | |
| 244 | if (privateFieldsAsProperties) { | |
| 245 | init = _core.types.callExpression(state.addHelper("classPrivateFieldLooseKey"), [_core.types.stringLiteral(name)]); | |
| 246 | } else if (privateFieldsAsSymbols) { | |
| 247 | init = _core.types.callExpression(_core.types.identifier("Symbol"), [_core.types.stringLiteral(name)]); | |
| 248 | } else if (!isStatic) { | |
| 249 | if (injectedIds.has(id.name)) continue; | |
| 250 | injectedIds.add(id.name); | |
| 251 | init = _core.types.newExpression(_core.types.identifier(isMethod && (!isGetterOrSetter || newHelpers(state)) ? "WeakSet" : "WeakMap"), []); | |
| 252 | } | |
| 253 | if (init) { | |
| 254 | if (!privateFieldsAsSymbols) { | |
| 255 | (0, _helperAnnotateAsPure.default)(init); | |
| 256 | } | |
| 257 | initNodes.push(_core.template.statement.ast`var ${id} = ${init}`); | |
| 258 | } | |
| 259 | } | |
| 260 | return initNodes; | |
| 261 | } | |
| 262 | ||
| 263 | // privateNameVisitorFactory | |
| 264 | function privateNameVisitorFactory(visitor) { | |
| 265 | const nestedVisitor = _traverse.visitors.environmentVisitor(Object.assign({}, visitor)); | |
| 266 | const privateNameVisitor = Object.assign({}, visitor, { | |
| 267 | Class(path) { | |
| 268 | const { | |
| 269 | privateNamesMap | |
| 270 | } = this; | |
| 271 | const body = path.get("body.body"); | |
| 272 | const visiblePrivateNames = new Map(privateNamesMap); | |
| 273 | const redeclared = []; | |
| 274 | for (const prop of body) { | |
| 275 | if (!prop.isPrivate()) continue; | |
| 276 | const { | |
| 277 | name | |
| 278 | } = prop.node.key.id; | |
| 279 | visiblePrivateNames.delete(name); | |
| 280 | redeclared.push(name); | |
| 281 | } | |
| 282 | if (!redeclared.length) { | |
| 283 | return; | |
| 284 | } | |
| 285 | path.get("body").traverse(nestedVisitor, Object.assign({}, this, { | |
| 286 | redeclared | |
| 287 | })); | |
| 288 | path.traverse(privateNameVisitor, Object.assign({}, this, { | |
| 289 | privateNamesMap: visiblePrivateNames | |
| 290 | })); | |
| 291 | path.skipKey("body"); | |
| 292 | } | |
| 293 | }); | |
| 294 | return privateNameVisitor; | |
| 295 | } | |
| 296 | ||
| 297 | // transformPrivateNamesUsage | |
| 298 | function transformPrivateNamesUsage(ref, path, privateNamesMap, { | |
| 299 | privateFieldsAsProperties, | |
| 300 | noUninitializedPrivateFieldAccess, | |
| 301 | noDocumentAll, | |
| 302 | innerBinding | |
| 303 | }, state) { | |
| 304 | if (!privateNamesMap.size) return; | |
| 305 | const body = path.get("body"); | |
| 306 | const handler = privateFieldsAsProperties ? privateNameHandlerLoose : privateNameHandlerSpec; | |
| 307 | (0, _helperMemberExpressionToFunctions.default)(body, privateNameVisitor, Object.assign({ | |
| 308 | privateNamesMap, | |
| 309 | classRef: ref, | |
| 310 | file: state | |
| 311 | }, handler, { | |
| 312 | noDocumentAll, | |
| 313 | noUninitializedPrivateFieldAccess, | |
| 314 | innerBinding | |
| 315 | })); | |
| 316 | body.traverse(privateInVisitor, { | |
| 317 | privateNamesMap, | |
| 318 | classRef: ref, | |
| 319 | file: state, | |
| 320 | privateFieldsAsProperties, | |
| 321 | innerBinding | |
| 322 | }); | |
| 323 | } |
src/file-viewer/server/src/cache.rs created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | // default | |
| 2 | function memberExpressionToFunctions(path, visitor, state) { | |
| 3 | path.traverse(visitor, Object.assign({}, handle, state, { | |
| 4 | memoiser: new AssignmentMemoiser() | |
| 5 | })); | |
| 6 | } |
src/file-viewer/server/src/config.rs created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | // default | |
| 2 | function optimiseCallExpression(callee, thisNode, args, optional) { | |
| 3 | if (args.length === 1 && isSpreadElement(args[0]) && isIdentifier(args[0].argument, { | |
| 4 | name: "arguments" | |
| 5 | })) { | |
| 6 | if (optional) { | |
| 7 | return optionalCallExpression(optionalMemberExpression(callee, identifier("apply"), false, true), [thisNode, args[0].argument], false); | |
| 8 | } | |
| 9 | return callExpression(memberExpression(callee, identifier("apply")), [thisNode, args[0].argument]); | |
| 10 | } else { | |
| 11 | if (optional) { | |
| 12 | return optionalCallExpression(optionalMemberExpression(callee, identifier("call"), false, true), [thisNode, ...args], false); | |
| 13 | } | |
| 14 | return callExpression(memberExpression(callee, identifier("call")), [thisNode, ...args]); | |
| 15 | } | |
| 16 | } |
src/file-viewer/server/src/database.rs created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | // default | |
| 2 | function annotateAsPure(pathOrNode) { | |
| 3 | const node = pathOrNode.node || pathOrNode; | |
| 4 | if (isPureAnnotated(node)) { | |
| 5 | return; | |
| 6 | } | |
| 7 | addComment(node, "leading", PURE_ANNOTATION); | |
| 8 | } |
src/file-viewer/server/src/main.rs created+20| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | // isTransparentExprWrapper | |
| 2 | function isTransparentExprWrapper(node) { | |
| 3 | return isTSAsExpression(node) || isTSSatisfiesExpression(node) || isTSTypeAssertion(node) || isTSNonNullExpression(node) || isTypeCastExpression(node) || isParenthesizedExpression(node); | |
| 4 | } | |
| 5 | ||
| 6 | // skipTransparentExprWrapperNodes | |
| 7 | function skipTransparentExprWrapperNodes(node) { | |
| 8 | while (isTransparentExprWrapper(node)) { | |
| 9 | node = node.expression; | |
| 10 | } | |
| 11 | return node; | |
| 12 | } | |
| 13 | ||
| 14 | // skipTransparentExprWrappers | |
| 15 | function skipTransparentExprWrappers(path) { | |
| 16 | while (isTransparentExprWrapper(path.node)) { | |
| 17 | path = path.get("expression"); | |
| 18 | } | |
| 19 | return path; | |
| 20 | } |
src/file-viewer/server/src/routes.rs created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | // assertFieldTransformed | |
| 2 | function assertFieldTransformed(path) { | |
| 3 | if (path.node.declare || false) { | |
| 4 | throw path.buildCodeFrameError(`TypeScript 'declare' fields must first be transformed by ` + `@babel/plugin-transform-typescript.\n` + `If you have already enabled that plugin (or '@babel/preset-typescript'), make sure ` + `that it runs before any plugin related to additional class features:\n` + ` - @babel/plugin-transform-class-properties\n` + ` - @babel/plugin-transform-private-methods\n` + ` - @babel/plugin-proposal-decorators`); | |
| 5 | } | |
| 6 | } |
src/file-viewer/server/src/routes_test.rs created+84| ... | ... | @@ -0,0 +1,84 @@ |
| 1 | // default | |
| 2 | function _default({ | |
| 3 | assertVersion, | |
| 4 | assumption | |
| 5 | }, { | |
| 6 | loose | |
| 7 | }, version, inherits) { | |
| 8 | var _assumption, _assumption2; | |
| 9 | { | |
| 10 | if (version === "2023-11" || version === "2023-05" || version === "2023-01") { | |
| 11 | assertVersion("^7.21.0"); | |
| 12 | } else if (version === "2021-12") { | |
| 13 | assertVersion("^7.16.0"); | |
| 14 | } else { | |
| 15 | assertVersion("^7.19.0"); | |
| 16 | } | |
| 17 | } | |
| 18 | const VISITED = new WeakSet(); | |
| 19 | const constantSuper = (_assumption = assumption("constantSuper")) != null ? _assumption : loose; | |
| 20 | const ignoreFunctionLength = (_assumption2 = assumption("ignoreFunctionLength")) != null ? _assumption2 : loose; | |
| 21 | const namedEvaluationVisitor = NamedEvaluationVisitoryFactory(isDecoratedAnonymousClassExpression, visitClass); | |
| 22 | function visitClass(path, state, className) { | |
| 23 | var _node$id; | |
| 24 | if (VISITED.has(path)) return; | |
| 25 | const { | |
| 26 | node | |
| 27 | } = path; | |
| 28 | className != null ? className : className = (_node$id = node.id) == null ? void 0 : _node$id.name; | |
| 29 | const newPath = transformClass(path, state, constantSuper, ignoreFunctionLength, className, namedEvaluationVisitor, version); | |
| 30 | if (newPath) { | |
| 31 | VISITED.add(newPath); | |
| 32 | return; | |
| 33 | } | |
| 34 | VISITED.add(path); | |
| 35 | } | |
| 36 | return { | |
| 37 | name: "proposal-decorators", | |
| 38 | inherits: inherits, | |
| 39 | visitor: Object.assign({ | |
| 40 | ExportDefaultDeclaration(path, state) { | |
| 41 | const { | |
| 42 | declaration | |
| 43 | } = path.node; | |
| 44 | if ((declaration == null ? void 0 : declaration.type) === "ClassDeclaration" && isDecorated(declaration)) { | |
| 45 | const isAnonymous = !declaration.id; | |
| 46 | { | |
| 47 | var _path$splitExportDecl; | |
| 48 | (_path$splitExportDecl = path.splitExportDeclaration) != null ? _path$splitExportDecl : path.splitExportDeclaration = require("@babel/traverse").NodePath.prototype.splitExportDeclaration; | |
| 49 | } | |
| 50 | const updatedVarDeclarationPath = path.splitExportDeclaration(); | |
| 51 | if (isAnonymous) { | |
| 52 | visitClass(updatedVarDeclarationPath, state, _core.types.stringLiteral("default")); | |
| 53 | } | |
| 54 | } | |
| 55 | }, | |
| 56 | ExportNamedDeclaration(path) { | |
| 57 | const { | |
| 58 | declaration | |
| 59 | } = path.node; | |
| 60 | if ((declaration == null ? void 0 : declaration.type) === "ClassDeclaration" && isDecorated(declaration)) { | |
| 61 | { | |
| 62 | var _path$splitExportDecl2; | |
| 63 | (_path$splitExportDecl2 = path.splitExportDeclaration) != null ? _path$splitExportDecl2 : path.splitExportDeclaration = require("@babel/traverse").NodePath.prototype.splitExportDeclaration; | |
| 64 | } | |
| 65 | path.splitExportDeclaration(); | |
| 66 | } | |
| 67 | }, | |
| 68 | Class(path, state) { | |
| 69 | visitClass(path, state, undefined); | |
| 70 | } | |
| 71 | }, namedEvaluationVisitor) | |
| 72 | }; | |
| 73 | } | |
| 74 | ||
| 75 | // hasDecorators | |
| 76 | function hasDecorators(node) { | |
| 77 | return hasOwnDecorators(node) || node.body.body.some(hasOwnDecorators); | |
| 78 | } | |
| 79 | ||
| 80 | // hasOwnDecorators | |
| 81 | function hasOwnDecorators(node) { | |
| 82 | var _node$decorators; | |
| 83 | return !!((_node$decorators = node.decorators) != null && _node$decorators.length); | |
| 84 | } |
src/file-viewer/server/src/server.rs created+89| ... | ... | @@ -0,0 +1,89 @@ |
| 1 | // extractComputedKeys | |
| 2 | function extractComputedKeys(path, computedPaths, file) { | |
| 3 | const { | |
| 4 | scope | |
| 5 | } = path; | |
| 6 | const declarations = []; | |
| 7 | const state = { | |
| 8 | classBinding: path.node.id && scope.getBinding(path.node.id.name), | |
| 9 | file | |
| 10 | }; | |
| 11 | for (const computedPath of computedPaths) { | |
| 12 | const computedKey = computedPath.get("key"); | |
| 13 | if (computedKey.isReferencedIdentifier()) { | |
| 14 | handleClassTDZ(computedKey, state); | |
| 15 | } else { | |
| 16 | computedKey.traverse(classFieldDefinitionEvaluationTDZVisitor, state); | |
| 17 | } | |
| 18 | const computedNode = computedPath.node; | |
| 19 | if (!computedKey.isConstantExpression()) { | |
| 20 | const assignment = memoiseComputedKey(computedKey.node, scope, scope.generateUidBasedOnNode(computedKey.node)); | |
| 21 | if (assignment) { | |
| 22 | declarations.push(_core.types.expressionStatement(assignment)); | |
| 23 | computedNode.key = _core.types.cloneNode(assignment.left); | |
| 24 | } | |
| 25 | } | |
| 26 | } | |
| 27 | return declarations; | |
| 28 | } | |
| 29 | ||
| 30 | // injectInitialization | |
| 31 | function injectInitialization(path, constructor, nodes, renamer, lastReturnsThis) { | |
| 32 | if (!nodes.length) return; | |
| 33 | const isDerived = !!path.node.superClass; | |
| 34 | if (!constructor) { | |
| 35 | const newConstructor = _core.types.classMethod("constructor", _core.types.identifier("constructor"), [], _core.types.blockStatement([])); | |
| 36 | if (isDerived) { | |
| 37 | newConstructor.params = [_core.types.restElement(_core.types.identifier("args"))]; | |
| 38 | newConstructor.body.body.push(_core.template.statement.ast`super(...args)`); | |
| 39 | } | |
| 40 | [constructor] = path.get("body").unshiftContainer("body", newConstructor); | |
| 41 | } | |
| 42 | if (renamer) { | |
| 43 | renamer(referenceVisitor, { | |
| 44 | scope: constructor.scope | |
| 45 | }); | |
| 46 | } | |
| 47 | if (isDerived) { | |
| 48 | const bareSupers = []; | |
| 49 | constructor.traverse(findBareSupers, bareSupers); | |
| 50 | let isFirst = true; | |
| 51 | for (const bareSuper of bareSupers) { | |
| 52 | if (isFirst) { | |
| 53 | isFirst = false; | |
| 54 | } else { | |
| 55 | nodes = nodes.map(n => _core.types.cloneNode(n)); | |
| 56 | } | |
| 57 | if (!bareSuper.parentPath.isExpressionStatement()) { | |
| 58 | const allNodes = [bareSuper.node, ...nodes.map(n => _core.types.toExpression(n))]; | |
| 59 | if (!lastReturnsThis) allNodes.push(_core.types.thisExpression()); | |
| 60 | bareSuper.replaceWith(_core.types.sequenceExpression(allNodes)); | |
| 61 | } else { | |
| 62 | bareSuper.insertAfter(nodes); | |
| 63 | } | |
| 64 | } | |
| 65 | } else { | |
| 66 | constructor.get("body").unshiftContainer("body", nodes); | |
| 67 | } | |
| 68 | } | |
| 69 | ||
| 70 | // memoiseComputedKey | |
| 71 | function memoiseComputedKey(keyNode, scope, hint) { | |
| 72 | const isUidReference = _core.types.isIdentifier(keyNode) && scope.hasUid(keyNode.name); | |
| 73 | if (isUidReference) { | |
| 74 | return; | |
| 75 | } | |
| 76 | const isMemoiseAssignment = _core.types.isAssignmentExpression(keyNode, { | |
| 77 | operator: "=" | |
| 78 | }) && _core.types.isIdentifier(keyNode.left) && scope.hasUid(keyNode.left.name); | |
| 79 | if (isMemoiseAssignment) { | |
| 80 | return _core.types.cloneNode(keyNode); | |
| 81 | } else { | |
| 82 | const ident = _core.types.identifier(hint); | |
| 83 | scope.push({ | |
| 84 | id: ident, | |
| 85 | kind: "let" | |
| 86 | }); | |
| 87 | return _core.types.assignmentExpression("=", _core.types.cloneNode(ident), keyNode); | |
| 88 | } | |
| 89 | } |
src/file-viewer/server/tests/end_to_end.rs created+101| ... | ... | @@ -0,0 +1,101 @@ |
| 1 | // FEATURES | |
| 2 | [object Object] | |
| 3 | ||
| 4 | // enableFeature | |
| 5 | function enableFeature(file, feature, loose) { | |
| 6 | if (!hasFeature(file, feature) || canIgnoreLoose(file, feature)) { | |
| 7 | file.set(featuresKey, file.get(featuresKey) | feature); | |
| 8 | if (loose === "#__internal__@babel/preset-env__prefer-true-but-false-is-ok-if-it-prevents-an-error") { | |
| 9 | setLoose(file, feature, true); | |
| 10 | file.set(looseLowPriorityKey, file.get(looseLowPriorityKey) | feature); | |
| 11 | } else if (loose === "#__internal__@babel/preset-env__prefer-false-but-true-is-ok-if-it-prevents-an-error") { | |
| 12 | setLoose(file, feature, false); | |
| 13 | file.set(looseLowPriorityKey, file.get(looseLowPriorityKey) | feature); | |
| 14 | } else { | |
| 15 | setLoose(file, feature, loose); | |
| 16 | } | |
| 17 | } | |
| 18 | let resolvedLoose; | |
| 19 | for (const [mask, name] of featuresSameLoose) { | |
| 20 | if (!hasFeature(file, mask)) continue; | |
| 21 | { | |
| 22 | if (canIgnoreLoose(file, mask)) continue; | |
| 23 | } | |
| 24 | const loose = isLoose(file, mask); | |
| 25 | if (resolvedLoose === !loose) { | |
| 26 | throw new Error("'loose' mode configuration must be the same for @babel/plugin-transform-class-properties, " + "@babel/plugin-transform-private-methods and " + "@babel/plugin-transform-private-property-in-object (when they are enabled)." + "\n\n" + getBabelShowConfigForHint(file)); | |
| 27 | } else { | |
| 28 | resolvedLoose = loose; | |
| 29 | { | |
| 30 | var higherPriorityPluginName = name; | |
| 31 | } | |
| 32 | } | |
| 33 | } | |
| 34 | if (resolvedLoose !== undefined) { | |
| 35 | for (const [mask, name] of featuresSameLoose) { | |
| 36 | if (hasFeature(file, mask) && isLoose(file, mask) !== resolvedLoose) { | |
| 37 | setLoose(file, mask, resolvedLoose); | |
| 38 | console.warn(`Though the "loose" option was set to "${!resolvedLoose}" in your @babel/preset-env ` + `config, it will not be used for ${name} since the "loose" mode option was set to ` + `"${resolvedLoose}" for ${higherPriorityPluginName}.\nThe "loose" option must be the ` + `same for @babel/plugin-transform-class-properties, @babel/plugin-transform-private-methods ` + `and @babel/plugin-transform-private-property-in-object (when they are enabled): you can ` + `silence this warning by explicitly adding\n` + `\t["${name}", { "loose": ${resolvedLoose} }]\n` + `to the "plugins" section of your Babel config.` + "\n\n" + getBabelShowConfigForHint(file)); | |
| 39 | } | |
| 40 | } | |
| 41 | } | |
| 42 | } | |
| 43 | ||
| 44 | // isLoose | |
| 45 | function isLoose(file, feature) { | |
| 46 | return !!(file.get(looseKey) & feature); | |
| 47 | } | |
| 48 | ||
| 49 | // shouldTransform | |
| 50 | function shouldTransform(path, file) { | |
| 51 | let decoratorPath = null; | |
| 52 | let publicFieldPath = null; | |
| 53 | let privateFieldPath = null; | |
| 54 | let privateMethodPath = null; | |
| 55 | let staticBlockPath = null; | |
| 56 | if ((0, _decorators.hasOwnDecorators)(path.node)) { | |
| 57 | decoratorPath = path.get("decorators.0"); | |
| 58 | } | |
| 59 | for (const el of path.get("body.body")) { | |
| 60 | if (!decoratorPath && (0, _decorators.hasOwnDecorators)(el.node)) { | |
| 61 | decoratorPath = el.get("decorators.0"); | |
| 62 | } | |
| 63 | if (!publicFieldPath && el.isClassProperty()) { | |
| 64 | publicFieldPath = el; | |
| 65 | } | |
| 66 | if (!privateFieldPath && el.isClassPrivateProperty()) { | |
| 67 | privateFieldPath = el; | |
| 68 | } | |
| 69 | if (!privateMethodPath && el.isClassPrivateMethod != null && el.isClassPrivateMethod()) { | |
| 70 | privateMethodPath = el; | |
| 71 | } | |
| 72 | if (!staticBlockPath && el.isStaticBlock != null && el.isStaticBlock()) { | |
| 73 | staticBlockPath = el; | |
| 74 | } | |
| 75 | } | |
| 76 | if (decoratorPath && privateFieldPath) { | |
| 77 | throw privateFieldPath.buildCodeFrameError("Private fields in decorated classes are not supported yet."); | |
| 78 | } | |
| 79 | if (decoratorPath && privateMethodPath) { | |
| 80 | throw privateMethodPath.buildCodeFrameError("Private methods in decorated classes are not supported yet."); | |
| 81 | } | |
| 82 | if (decoratorPath && !hasFeature(file, FEATURES.decorators)) { | |
| 83 | throw path.buildCodeFrameError("Decorators are not enabled." + "\nIf you are using " + '["@babel/plugin-proposal-decorators", { "version": "legacy" }], ' + 'make sure it comes *before* "@babel/plugin-transform-class-properties" ' + "and enable loose mode, like so:\n" + '\t["@babel/plugin-proposal-decorators", { "version": "legacy" }]\n' + '\t["@babel/plugin-transform-class-properties", { "loose": true }]'); | |
| 84 | } | |
| 85 | if (privateMethodPath && !hasFeature(file, FEATURES.privateMethods)) { | |
| 86 | throw privateMethodPath.buildCodeFrameError("Class private methods are not enabled. " + "Please add `@babel/plugin-transform-private-methods` to your configuration."); | |
| 87 | } | |
| 88 | if ((publicFieldPath || privateFieldPath) && !hasFeature(file, FEATURES.fields) && !hasFeature(file, FEATURES.privateMethods)) { | |
| 89 | throw path.buildCodeFrameError("Class fields are not enabled. " + "Please add `@babel/plugin-transform-class-properties` to your configuration."); | |
| 90 | } | |
| 91 | if (staticBlockPath && !hasFeature(file, FEATURES.staticBlocks)) { | |
| 92 | throw path.buildCodeFrameError("Static class blocks are not enabled. " + "Please add `@babel/plugin-transform-class-static-block` to your configuration."); | |
| 93 | } | |
| 94 | if (decoratorPath || privateMethodPath || staticBlockPath) { | |
| 95 | return true; | |
| 96 | } | |
| 97 | if ((publicFieldPath || privateFieldPath) && hasFeature(file, FEATURES.fields)) { | |
| 98 | return true; | |
| 99 | } | |
| 100 | return false; | |
| 101 | } |
src/file-viewer/server/tests/manual_test.cjs created+53| ... | ... | @@ -0,0 +1,53 @@ |
| 1 | // EXPORTED_CONST_ENUMS_IN_NAMESPACE | |
| 2 | [object WeakSet] | |
| 3 | ||
| 4 | // default | |
| 5 | function transpileConstEnum(path, t) { | |
| 6 | const { | |
| 7 | name | |
| 8 | } = path.node.id; | |
| 9 | const parentIsExport = path.parentPath.isExportNamedDeclaration(); | |
| 10 | let isExported = parentIsExport; | |
| 11 | if (!isExported && t.isProgram(path.parent)) { | |
| 12 | isExported = path.parent.body.some(stmt => t.isExportNamedDeclaration(stmt) && stmt.exportKind !== "type" && !stmt.source && stmt.specifiers.some(spec => t.isExportSpecifier(spec) && spec.exportKind !== "type" && spec.local.name === name)); | |
| 13 | } | |
| 14 | const { | |
| 15 | enumValues: entries | |
| 16 | } = (0, _enum.translateEnumValues)(path, t); | |
| 17 | if (isExported || EXPORTED_CONST_ENUMS_IN_NAMESPACE.has(path.node)) { | |
| 18 | const obj = t.objectExpression(entries.map(([name, value]) => t.objectProperty(t.isValidIdentifier(name) ? t.identifier(name) : t.stringLiteral(name), value))); | |
| 19 | if (path.scope.hasOwnBinding(name)) { | |
| 20 | (parentIsExport ? path.parentPath : path).replaceWith(t.expressionStatement(t.callExpression(t.memberExpression(t.identifier("Object"), t.identifier("assign")), [path.node.id, obj]))); | |
| 21 | } else { | |
| 22 | path.replaceWith(t.variableDeclaration("var", [t.variableDeclarator(path.node.id, obj)])); | |
| 23 | path.scope.registerDeclaration(path); | |
| 24 | } | |
| 25 | return; | |
| 26 | } | |
| 27 | const entriesMap = new Map(entries); | |
| 28 | path.scope.path.traverse({ | |
| 29 | Scope(path) { | |
| 30 | if (path.scope.hasOwnBinding(name)) path.skip(); | |
| 31 | }, | |
| 32 | MemberExpression(path) { | |
| 33 | if (!t.isIdentifier(path.node.object, { | |
| 34 | name | |
| 35 | })) return; | |
| 36 | let key; | |
| 37 | if (path.node.computed) { | |
| 38 | if (t.isStringLiteral(path.node.property)) { | |
| 39 | key = path.node.property.value; | |
| 40 | } else { | |
| 41 | return; | |
| 42 | } | |
| 43 | } else if (t.isIdentifier(path.node.property)) { | |
| 44 | key = path.node.property.name; | |
| 45 | } else { | |
| 46 | return; | |
| 47 | } | |
| 48 | if (!entriesMap.has(key)) return; | |
| 49 | path.replaceWith(t.cloneNode(entriesMap.get(key))); | |
| 50 | } | |
| 51 | }); | |
| 52 | path.remove(); | |
| 53 | } |
src/file-viewer/server/tests/setup_test_env.sh created+138| ... | ... | @@ -0,0 +1,138 @@ |
| 1 | // default | |
| 2 | function transpileEnum(path, t) { | |
| 3 | const { | |
| 4 | node, | |
| 5 | parentPath | |
| 6 | } = path; | |
| 7 | if (node.declare) { | |
| 8 | path.remove(); | |
| 9 | return; | |
| 10 | } | |
| 11 | const name = node.id.name; | |
| 12 | const { | |
| 13 | fill, | |
| 14 | data, | |
| 15 | isPure | |
| 16 | } = enumFill(path, t, node.id); | |
| 17 | switch (parentPath.type) { | |
| 18 | case "BlockStatement": | |
| 19 | case "ExportNamedDeclaration": | |
| 20 | case "Program": | |
| 21 | { | |
| 22 | const isGlobal = t.isProgram(path.parent); | |
| 23 | const isSeen = seen(parentPath); | |
| 24 | let init = t.objectExpression([]); | |
| 25 | if (isSeen || isGlobal) { | |
| 26 | init = t.logicalExpression("||", t.cloneNode(fill.ID), init); | |
| 27 | } | |
| 28 | const enumIIFE = buildEnumWrapper(Object.assign({}, fill, { | |
| 29 | INIT: init | |
| 30 | })); | |
| 31 | if (isPure) (0, _helperAnnotateAsPure.default)(enumIIFE); | |
| 32 | if (isSeen) { | |
| 33 | const toReplace = parentPath.isExportDeclaration() ? parentPath : path; | |
| 34 | toReplace.replaceWith(t.expressionStatement(t.assignmentExpression("=", t.cloneNode(node.id), enumIIFE))); | |
| 35 | } else { | |
| 36 | path.scope.registerDeclaration(path.replaceWith(t.variableDeclaration(isGlobal ? "var" : "let", [t.variableDeclarator(node.id, enumIIFE)]))[0]); | |
| 37 | } | |
| 38 | ENUMS.set(path.scope.getBindingIdentifier(name), data); | |
| 39 | break; | |
| 40 | } | |
| 41 | default: | |
| 42 | throw new Error(`Unexpected enum parent '${path.parent.type}`); | |
| 43 | } | |
| 44 | function seen(parentPath) { | |
| 45 | if (parentPath.isExportDeclaration()) { | |
| 46 | return seen(parentPath.parentPath); | |
| 47 | } | |
| 48 | if (parentPath.getData(name)) { | |
| 49 | return true; | |
| 50 | } else { | |
| 51 | parentPath.setData(name, true); | |
| 52 | return false; | |
| 53 | } | |
| 54 | } | |
| 55 | } | |
| 56 | ||
| 57 | // isSyntacticallyString | |
| 58 | function isSyntacticallyString(expr) { | |
| 59 | expr = (0, _helperSkipTransparentExpressionWrappers.skipTransparentExprWrapperNodes)(expr); | |
| 60 | switch (expr.type) { | |
| 61 | case "BinaryExpression": | |
| 62 | { | |
| 63 | const left = expr.left; | |
| 64 | const right = expr.right; | |
| 65 | return expr.operator === "+" && (isSyntacticallyString(left) || isSyntacticallyString(right)); | |
| 66 | } | |
| 67 | case "TemplateLiteral": | |
| 68 | case "StringLiteral": | |
| 69 | return true; | |
| 70 | } | |
| 71 | return false; | |
| 72 | } | |
| 73 | ||
| 74 | // translateEnumValues | |
| 75 | function translateEnumValues(path, t) { | |
| 76 | var _ENUMS$get; | |
| 77 | const bindingIdentifier = path.scope.getBindingIdentifier(path.node.id.name); | |
| 78 | const seen = (_ENUMS$get = ENUMS.get(bindingIdentifier)) != null ? _ENUMS$get : new Map(); | |
| 79 | let constValue = -1; | |
| 80 | let lastName; | |
| 81 | let isPure = true; | |
| 82 | const enumMembers = path.get("members"); | |
| 83 | const enumValues = enumMembers.map(memberPath => { | |
| 84 | const member = memberPath.node; | |
| 85 | const name = t.isIdentifier(member.id) ? member.id.name : member.id.value; | |
| 86 | const initializerPath = memberPath.get("initializer"); | |
| 87 | const initializer = member.initializer; | |
| 88 | let value; | |
| 89 | if (initializer) { | |
| 90 | constValue = computeConstantValue(initializerPath, seen); | |
| 91 | if (constValue !== undefined) { | |
| 92 | seen.set(name, constValue); | |
| 93 | _assert(typeof constValue === "number" || typeof constValue === "string"); | |
| 94 | if (constValue === Infinity || Number.isNaN(constValue)) { | |
| 95 | value = t.identifier(String(constValue)); | |
| 96 | } else if (constValue === -Infinity) { | |
| 97 | value = t.unaryExpression("-", t.identifier("Infinity")); | |
| 98 | } else { | |
| 99 | value = t.valueToNode(constValue); | |
| 100 | } | |
| 101 | } else { | |
| 102 | isPure && (isPure = initializerPath.isPure()); | |
| 103 | if (initializerPath.isReferencedIdentifier()) { | |
| 104 | ReferencedIdentifier(initializerPath, { | |
| 105 | t, | |
| 106 | seen, | |
| 107 | path | |
| 108 | }); | |
| 109 | } else { | |
| 110 | initializerPath.traverse(enumSelfReferenceVisitor, { | |
| 111 | t, | |
| 112 | seen, | |
| 113 | path | |
| 114 | }); | |
| 115 | } | |
| 116 | value = initializerPath.node; | |
| 117 | seen.set(name, undefined); | |
| 118 | } | |
| 119 | } else if (typeof constValue === "number") { | |
| 120 | constValue += 1; | |
| 121 | value = t.numericLiteral(constValue); | |
| 122 | seen.set(name, constValue); | |
| 123 | } else if (typeof constValue === "string") { | |
| 124 | throw path.buildCodeFrameError("Enum member must have initializer."); | |
| 125 | } else { | |
| 126 | const lastRef = t.memberExpression(t.cloneNode(path.node.id), t.stringLiteral(lastName), true); | |
| 127 | value = t.binaryExpression("+", t.numericLiteral(1), lastRef); | |
| 128 | seen.set(name, undefined); | |
| 129 | } | |
| 130 | lastName = name; | |
| 131 | return [name, value]; | |
| 132 | }); | |
| 133 | return { | |
| 134 | isPure, | |
| 135 | data: seen, | |
| 136 | enumValues | |
| 137 | }; | |
| 138 | } |
src/file-viewer/share.ts created+17| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | // GLOBAL_TYPES | |
| 2 | [object WeakMap] | |
| 3 | ||
| 4 | // isGlobalType | |
| 5 | function isGlobalType({ | |
| 6 | scope | |
| 7 | }, name) { | |
| 8 | if (scope.hasBinding(name)) return false; | |
| 9 | if (GLOBAL_TYPES.get(scope).has(name)) return true; | |
| 10 | console.warn(`The exported identifier "${name}" is not declared in Babel's scope tracker\n` + `as a JavaScript value binding, and "@babel/plugin-transform-typescript"\n` + `never encountered it as a TypeScript type declaration.\n` + `It will be treated as a JavaScript value.\n\n` + `This problem is likely caused by another plugin injecting\n` + `"${name}" without registering it in the scope tracker. If you are the author\n` + ` of that plugin, please use "scope.registerDeclaration(declarationPath)".`); | |
| 11 | return false; | |
| 12 | } | |
| 13 | ||
| 14 | // registerGlobalType | |
| 15 | function registerGlobalType(programScope, name) { | |
| 16 | GLOBAL_TYPES.get(programScope).add(name); | |
| 17 | } |
src/file-viewer/static/captcha/image/0.jpeg created+44| ... | ... | @@ -0,0 +1,44 @@ |
| 1 | // makeConfigAPI | |
| 2 | function makeConfigAPI(cache) { | |
| 3 | const env = value => cache.using(data => { | |
| 4 | if (value === undefined) return data.envName; | |
| 5 | if (typeof value === "function") { | |
| 6 | return (0, _caching.assertSimpleType)(value(data.envName)); | |
| 7 | } | |
| 8 | return (Array.isArray(value) ? value : [value]).some(entry => { | |
| 9 | if (typeof entry !== "string") { | |
| 10 | throw new Error("Unexpected non-string value"); | |
| 11 | } | |
| 12 | return entry === data.envName; | |
| 13 | }); | |
| 14 | }); | |
| 15 | const caller = cb => cache.using(data => (0, _caching.assertSimpleType)(cb(data.caller))); | |
| 16 | return { | |
| 17 | version: _index.version, | |
| 18 | cache: cache.simple(), | |
| 19 | env, | |
| 20 | async: () => false, | |
| 21 | caller, | |
| 22 | assertVersion | |
| 23 | }; | |
| 24 | } | |
| 25 | ||
| 26 | // makePluginAPI | |
| 27 | function makePluginAPI(cache, externalDependencies) { | |
| 28 | const assumption = name => cache.using(data => data.assumptions[name]); | |
| 29 | return Object.assign({}, makePresetAPI(cache, externalDependencies), { | |
| 30 | assumption | |
| 31 | }); | |
| 32 | } | |
| 33 | ||
| 34 | // makePresetAPI | |
| 35 | function makePresetAPI(cache, externalDependencies) { | |
| 36 | const targets = () => JSON.parse(cache.using(data => JSON.stringify(data.targets))); | |
| 37 | const addExternalDependency = ref => { | |
| 38 | externalDependencies.push(ref); | |
| 39 | }; | |
| 40 | return Object.assign({}, makeConfigAPI(cache), { | |
| 41 | targets, | |
| 42 | addExternalDependency | |
| 43 | }); | |
| 44 | } |
src/file-viewer/static/captcha/image/1.jpeg created+47| ... | ... | @@ -0,0 +1,47 @@ |
| 1 | // default | |
| 2 | function* loadCodeDefault(filepath, loader, esmError, tlaError) { | |
| 3 | let async; | |
| 4 | const ext = _path().extname(filepath); | |
| 5 | const isTS = ext === ".ts" || ext === ".cts" || ext === ".mts"; | |
| 6 | const type = SUPPORTED_EXTENSIONS[hasOwnProperty.call(SUPPORTED_EXTENSIONS, ext) ? ext : ".js"]; | |
| 7 | const pattern = `${loader} ${type}`; | |
| 8 | switch (pattern) { | |
| 9 | case "require cjs": | |
| 10 | case "auto cjs": | |
| 11 | if (isTS) { | |
| 12 | return ensureTsSupport(filepath, ext, () => loadCjsDefault(filepath)); | |
| 13 | } else { | |
| 14 | return loadCjsDefault(filepath, arguments[2]); | |
| 15 | } | |
| 16 | case "auto unknown": | |
| 17 | case "require unknown": | |
| 18 | case "require esm": | |
| 19 | try { | |
| 20 | if (isTS) { | |
| 21 | return ensureTsSupport(filepath, ext, () => loadCjsDefault(filepath)); | |
| 22 | } else { | |
| 23 | return loadCjsDefault(filepath, arguments[2]); | |
| 24 | } | |
| 25 | } catch (e) { | |
| 26 | if (e.code === "ERR_REQUIRE_ASYNC_MODULE" || e.code === "ERR_REQUIRE_CYCLE_MODULE" && asyncModules.has(filepath)) { | |
| 27 | asyncModules.add(filepath); | |
| 28 | if (!(async != null ? async : async = yield* (0, _async.isAsync)())) { | |
| 29 | throw new _configError.default(tlaError, filepath); | |
| 30 | } | |
| 31 | } else if (e.code === "ERR_REQUIRE_ESM" || type === "esm") {} else { | |
| 32 | throw e; | |
| 33 | } | |
| 34 | } | |
| 35 | case "auto esm": | |
| 36 | if (async != null ? async : async = yield* (0, _async.isAsync)()) { | |
| 37 | const promise = isTS ? ensureTsSupport(filepath, ext, () => loadMjsFromPath(filepath)) : loadMjsFromPath(filepath); | |
| 38 | return (yield* (0, _async.waitFor)(promise)).default; | |
| 39 | } | |
| 40 | throw new _configError.default(esmError, filepath); | |
| 41 | default: | |
| 42 | throw new Error("Internal Babel error: unreachable code."); | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 46 | // supportsESM | |
| 47 | true |
src/file-viewer/static/captcha/image/10.jpeg created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | // transformFile | |
| 2 | function transformFile(...args) { | |
| 3 | transformFileRunner.errback(...args); | |
| 4 | } | |
| 5 | ||
| 6 | // transformFileAsync | |
| 7 | function transformFileAsync(...args) { | |
| 8 | return transformFileRunner.async(...args); | |
| 9 | } | |
| 10 | ||
| 11 | // transformFileSync | |
| 12 | function transformFileSync(...args) { | |
| 13 | return transformFileRunner.sync(...args); | |
| 14 | } |
src/file-viewer/static/captcha/image/11.jpeg created+73| ... | ... | @@ -0,0 +1,73 @@ |
| 1 | // createConfigItem | |
| 2 | function createConfigItem(target, options, callback) { | |
| 3 | if (callback !== undefined) { | |
| 4 | (0, _rewriteStackTrace.beginHiddenCallStack)(createConfigItemRunner.errback)(target, options, callback); | |
| 5 | } else if (typeof options === "function") { | |
| 6 | (0, _rewriteStackTrace.beginHiddenCallStack)(createConfigItemRunner.errback)(target, undefined, callback); | |
| 7 | } else { | |
| 8 | { | |
| 9 | return createConfigItemSync(target, options); | |
| 10 | } | |
| 11 | } | |
| 12 | } | |
| 13 | ||
| 14 | // createConfigItemAsync | |
| 15 | function createConfigItemAsync(...args) { | |
| 16 | return (0, _rewriteStackTrace.beginHiddenCallStack)(createConfigItemRunner.async)(...args); | |
| 17 | } | |
| 18 | ||
| 19 | // createConfigItemSync | |
| 20 | function createConfigItemSync(...args) { | |
| 21 | return (0, _rewriteStackTrace.beginHiddenCallStack)(createConfigItemRunner.sync)(...args); | |
| 22 | } | |
| 23 | ||
| 24 | // default | |
| 25 | function(...args) { | |
| 26 | return genFn.apply(this, args); | |
| 27 | } | |
| 28 | ||
| 29 | // loadOptions | |
| 30 | function loadOptions(opts, callback) { | |
| 31 | if (callback !== undefined) { | |
| 32 | (0, _rewriteStackTrace.beginHiddenCallStack)(loadOptionsRunner.errback)(opts, callback); | |
| 33 | } else if (typeof opts === "function") { | |
| 34 | (0, _rewriteStackTrace.beginHiddenCallStack)(loadOptionsRunner.errback)(undefined, opts); | |
| 35 | } else { | |
| 36 | { | |
| 37 | return loadOptionsSync(opts); | |
| 38 | } | |
| 39 | } | |
| 40 | } | |
| 41 | ||
| 42 | // loadOptionsAsync | |
| 43 | function loadOptionsAsync(...args) { | |
| 44 | return (0, _rewriteStackTrace.beginHiddenCallStack)(loadOptionsRunner.async)(...args); | |
| 45 | } | |
| 46 | ||
| 47 | // loadOptionsSync | |
| 48 | function loadOptionsSync(...args) { | |
| 49 | return (0, _rewriteStackTrace.beginHiddenCallStack)(loadOptionsRunner.sync)(...args); | |
| 50 | } | |
| 51 | ||
| 52 | // loadPartialConfig | |
| 53 | function loadPartialConfig(opts, callback) { | |
| 54 | if (callback !== undefined) { | |
| 55 | (0, _rewriteStackTrace.beginHiddenCallStack)(loadPartialConfigRunner.errback)(opts, callback); | |
| 56 | } else if (typeof opts === "function") { | |
| 57 | (0, _rewriteStackTrace.beginHiddenCallStack)(loadPartialConfigRunner.errback)(undefined, opts); | |
| 58 | } else { | |
| 59 | { | |
| 60 | return loadPartialConfigSync(opts); | |
| 61 | } | |
| 62 | } | |
| 63 | } | |
| 64 | ||
| 65 | // loadPartialConfigAsync | |
| 66 | function loadPartialConfigAsync(...args) { | |
| 67 | return (0, _rewriteStackTrace.beginHiddenCallStack)(loadPartialConfigRunner.async)(...args); | |
| 68 | } | |
| 69 | ||
| 70 | // loadPartialConfigSync | |
| 71 | function loadPartialConfigSync(...args) { | |
| 72 | return (0, _rewriteStackTrace.beginHiddenCallStack)(loadPartialConfigRunner.sync)(...args); | |
| 73 | } |
src/file-viewer/static/captcha/image/12.jpeg created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | // default | |
| 2 | function(...args) { | |
| 3 | return genFn.apply(this, args); | |
| 4 | } |
src/file-viewer/static/captcha/image/13.jpeg created+23| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | // default | |
| 2 | class Plugin { | |
| 3 | constructor(plugin, options, key, externalDependencies = (0, _deepArray.finalize)([])) { | |
| 4 | this.key = void 0; | |
| 5 | this.manipulateOptions = void 0; | |
| 6 | this.post = void 0; | |
| 7 | this.pre = void 0; | |
| 8 | this.visitor = void 0; | |
| 9 | this.parserOverride = void 0; | |
| 10 | this.generatorOverride = void 0; | |
| 11 | this.options = void 0; | |
| 12 | this.externalDependencies = void 0; | |
| 13 | this.key = plugin.name || key; | |
| 14 | this.manipulateOptions = plugin.manipulateOptions; | |
| 15 | this.post = plugin.post; | |
| 16 | this.pre = plugin.pre; | |
| 17 | this.visitor = plugin.visitor || {}; | |
| 18 | this.parserOverride = plugin.parserOverride; | |
| 19 | this.generatorOverride = plugin.generatorOverride; | |
| 20 | this.options = options; | |
| 21 | this.externalDependencies = externalDependencies; | |
| 22 | } | |
| 23 | } |
src/file-viewer/static/captcha/image/14.jpeg created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | // finalize | |
| 2 | function finalize(deepArr) { | |
| 3 | return Object.freeze(deepArr); | |
| 4 | } | |
| 5 | ||
| 6 | // flattenToSet | |
| 7 | function flattenToSet(arr) { | |
| 8 | const result = new Set(); | |
| 9 | const stack = [arr]; | |
| 10 | while (stack.length > 0) { | |
| 11 | for (const el of stack.pop()) { | |
| 12 | if (Array.isArray(el)) stack.push(el);else result.add(el); | |
| 13 | } | |
| 14 | } | |
| 15 | return result; | |
| 16 | } |
src/file-viewer/static/captcha/image/15.jpeg created+24| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | // createConfigItem | |
| 2 | function* createConfigItem(value, { | |
| 3 | dirname = ".", | |
| 4 | type | |
| 5 | } = {}) { | |
| 6 | const descriptor = yield* (0, _configDescriptors.createDescriptor)(value, _path().resolve(dirname), { | |
| 7 | type, | |
| 8 | alias: "programmatic item" | |
| 9 | }); | |
| 10 | return createItemFromDescriptor(descriptor); | |
| 11 | } | |
| 12 | ||
| 13 | // createItemFromDescriptor | |
| 14 | function createItemFromDescriptor(desc) { | |
| 15 | return new ConfigItem(desc); | |
| 16 | } | |
| 17 | ||
| 18 | // getItemDescriptor | |
| 19 | function getItemDescriptor(item) { | |
| 20 | if (item != null && item[CONFIG_ITEM_BRAND]) { | |
| 21 | return item._descriptor; | |
| 22 | } | |
| 23 | return undefined; | |
| 24 | } |
src/file-viewer/static/captcha/image/16.jpeg created+86| ... | ... | @@ -0,0 +1,86 @@ |
| 1 | // createCachedDescriptors | |
| 2 | function createCachedDescriptors(dirname, options, alias) { | |
| 3 | const { | |
| 4 | plugins, | |
| 5 | presets, | |
| 6 | passPerPreset | |
| 7 | } = options; | |
| 8 | return { | |
| 9 | options: optionsWithResolvedBrowserslistConfigFile(options, dirname), | |
| 10 | plugins: plugins ? () => createCachedPluginDescriptors(plugins, dirname)(alias) : () => handlerOf([]), | |
| 11 | presets: presets ? () => createCachedPresetDescriptors(presets, dirname)(alias)(!!passPerPreset) : () => handlerOf([]) | |
| 12 | }; | |
| 13 | } | |
| 14 | ||
| 15 | // createDescriptor | |
| 16 | function* createDescriptor(pair, dirname, { | |
| 17 | type, | |
| 18 | alias, | |
| 19 | ownPass | |
| 20 | }) { | |
| 21 | const desc = (0, _item.getItemDescriptor)(pair); | |
| 22 | if (desc) { | |
| 23 | return desc; | |
| 24 | } | |
| 25 | let name; | |
| 26 | let options; | |
| 27 | let value = pair; | |
| 28 | if (Array.isArray(value)) { | |
| 29 | if (value.length === 3) { | |
| 30 | [value, options, name] = value; | |
| 31 | } else { | |
| 32 | [value, options] = value; | |
| 33 | } | |
| 34 | } | |
| 35 | let file = undefined; | |
| 36 | let filepath = null; | |
| 37 | if (typeof value === "string") { | |
| 38 | if (typeof type !== "string") { | |
| 39 | throw new Error("To resolve a string-based item, the type of item must be given"); | |
| 40 | } | |
| 41 | const resolver = type === "plugin" ? _index.loadPlugin : _index.loadPreset; | |
| 42 | const request = value; | |
| 43 | ({ | |
| 44 | filepath, | |
| 45 | value | |
| 46 | } = yield* resolver(value, dirname)); | |
| 47 | file = { | |
| 48 | request, | |
| 49 | resolved: filepath | |
| 50 | }; | |
| 51 | } | |
| 52 | if (!value) { | |
| 53 | throw new Error(`Unexpected falsy value: ${String(value)}`); | |
| 54 | } | |
| 55 | if (typeof value === "object" && value.__esModule) { | |
| 56 | if (value.default) { | |
| 57 | value = value.default; | |
| 58 | } else { | |
| 59 | throw new Error("Must export a default export when using ES6 modules."); | |
| 60 | } | |
| 61 | } | |
| 62 | if (typeof value !== "object" && typeof value !== "function") { | |
| 63 | throw new Error(`Unsupported format: ${typeof value}. Expected an object or a function.`); | |
| 64 | } | |
| 65 | if (filepath !== null && typeof value === "object" && value) { | |
| 66 | throw new Error(`Plugin/Preset files are not allowed to export objects, only functions. In ${filepath}`); | |
| 67 | } | |
| 68 | return { | |
| 69 | name, | |
| 70 | alias: filepath || alias, | |
| 71 | value, | |
| 72 | options, | |
| 73 | dirname, | |
| 74 | ownPass, | |
| 75 | file | |
| 76 | }; | |
| 77 | } | |
| 78 | ||
| 79 | // createUncachedDescriptors | |
| 80 | function createUncachedDescriptors(dirname, options, alias) { | |
| 81 | return { | |
| 82 | options: optionsWithResolvedBrowserslistConfigFile(options, dirname), | |
| 83 | plugins: (0, _functional.once)(() => createPluginDescriptors(options.plugins || [], dirname, alias)), | |
| 84 | presets: (0, _functional.once)(() => createPresetDescriptors(options.presets || [], dirname, alias, !!options.passPerPreset)) | |
| 85 | }; | |
| 86 | } |
src/file-viewer/static/captcha/image/17.jpeg created+49| ... | ... | @@ -0,0 +1,49 @@ |
| 1 | // once | |
| 2 | function once(fn) { | |
| 3 | let result; | |
| 4 | let resultP; | |
| 5 | let promiseReferenced = false; | |
| 6 | return function* () { | |
| 7 | if (!result) { | |
| 8 | if (resultP) { | |
| 9 | promiseReferenced = true; | |
| 10 | return yield* (0, _async.waitFor)(resultP); | |
| 11 | } | |
| 12 | if (!(yield* (0, _async.isAsync)())) { | |
| 13 | try { | |
| 14 | result = { | |
| 15 | ok: true, | |
| 16 | value: yield* fn() | |
| 17 | }; | |
| 18 | } catch (error) { | |
| 19 | result = { | |
| 20 | ok: false, | |
| 21 | value: error | |
| 22 | }; | |
| 23 | } | |
| 24 | } else { | |
| 25 | let resolve, reject; | |
| 26 | resultP = new Promise((res, rej) => { | |
| 27 | resolve = res; | |
| 28 | reject = rej; | |
| 29 | }); | |
| 30 | try { | |
| 31 | result = { | |
| 32 | ok: true, | |
| 33 | value: yield* fn() | |
| 34 | }; | |
| 35 | resultP = null; | |
| 36 | if (promiseReferenced) resolve(result.value); | |
| 37 | } catch (error) { | |
| 38 | result = { | |
| 39 | ok: false, | |
| 40 | value: error | |
| 41 | }; | |
| 42 | resultP = null; | |
| 43 | if (promiseReferenced) reject(error); | |
| 44 | } | |
| 45 | } | |
| 46 | } | |
| 47 | if (result.ok) return result.value;else throw result.value; | |
| 48 | }; | |
| 49 | } |
src/file-viewer/static/captcha/image/2.jpeg created+39| ... | ... | @@ -0,0 +1,39 @@ |
| 1 | // resolveBrowserslistConfigFile | |
| 2 | function resolveBrowserslistConfigFile(browserslistConfigFile, configFileDir) { | |
| 3 | return _path().resolve(configFileDir, browserslistConfigFile); | |
| 4 | } | |
| 5 | ||
| 6 | // resolveTargets | |
| 7 | function resolveTargets(options, root) { | |
| 8 | const optTargets = options.targets; | |
| 9 | let targets; | |
| 10 | if (typeof optTargets === "string" || Array.isArray(optTargets)) { | |
| 11 | targets = { | |
| 12 | browsers: optTargets | |
| 13 | }; | |
| 14 | } else if (optTargets) { | |
| 15 | if ("esmodules" in optTargets) { | |
| 16 | targets = Object.assign({}, optTargets, { | |
| 17 | esmodules: "intersect" | |
| 18 | }); | |
| 19 | } else { | |
| 20 | targets = optTargets; | |
| 21 | } | |
| 22 | } | |
| 23 | const { | |
| 24 | browserslistConfigFile | |
| 25 | } = options; | |
| 26 | let configFile; | |
| 27 | let ignoreBrowserslistConfig = false; | |
| 28 | if (typeof browserslistConfigFile === "string") { | |
| 29 | configFile = browserslistConfigFile; | |
| 30 | } else { | |
| 31 | ignoreBrowserslistConfig = browserslistConfigFile === false; | |
| 32 | } | |
| 33 | return (0, _helperCompilationTargets().default)(targets, { | |
| 34 | ignoreBrowserslistConfig, | |
| 35 | configFile, | |
| 36 | configPath: root, | |
| 37 | browserslistEnv: options.browserslistEnv | |
| 38 | }); | |
| 39 | } |
src/file-viewer/static/captcha/image/3.jpeg created+162| ... | ... | @@ -0,0 +1,162 @@ |
| 1 | // buildPresetChain | |
| 2 | function* buildPresetChain(arg, context) { | |
| 3 | const chain = yield* buildPresetChainWalker(arg, context); | |
| 4 | if (!chain) return null; | |
| 5 | return { | |
| 6 | plugins: dedupDescriptors(chain.plugins), | |
| 7 | presets: dedupDescriptors(chain.presets), | |
| 8 | options: chain.options.map(o => normalizeOptions(o)), | |
| 9 | files: new Set() | |
| 10 | }; | |
| 11 | } | |
| 12 | ||
| 13 | // buildPresetChainWalker | |
| 14 | function* chainWalker(input, context, files = new Set(), baseLogger) { | |
| 15 | const { | |
| 16 | dirname | |
| 17 | } = input; | |
| 18 | const flattenedConfigs = []; | |
| 19 | const rootOpts = root(input); | |
| 20 | if (configIsApplicable(rootOpts, dirname, context, input.filepath)) { | |
| 21 | flattenedConfigs.push({ | |
| 22 | config: rootOpts, | |
| 23 | envName: undefined, | |
| 24 | index: undefined | |
| 25 | }); | |
| 26 | const envOpts = env(input, context.envName); | |
| 27 | if (envOpts && configIsApplicable(envOpts, dirname, context, input.filepath)) { | |
| 28 | flattenedConfigs.push({ | |
| 29 | config: envOpts, | |
| 30 | envName: context.envName, | |
| 31 | index: undefined | |
| 32 | }); | |
| 33 | } | |
| 34 | (rootOpts.options.overrides || []).forEach((_, index) => { | |
| 35 | const overrideOps = overrides(input, index); | |
| 36 | if (configIsApplicable(overrideOps, dirname, context, input.filepath)) { | |
| 37 | flattenedConfigs.push({ | |
| 38 | config: overrideOps, | |
| 39 | index, | |
| 40 | envName: undefined | |
| 41 | }); | |
| 42 | const overrideEnvOpts = overridesEnv(input, index, context.envName); | |
| 43 | if (overrideEnvOpts && configIsApplicable(overrideEnvOpts, dirname, context, input.filepath)) { | |
| 44 | flattenedConfigs.push({ | |
| 45 | config: overrideEnvOpts, | |
| 46 | index, | |
| 47 | envName: context.envName | |
| 48 | }); | |
| 49 | } | |
| 50 | } | |
| 51 | }); | |
| 52 | } | |
| 53 | if (flattenedConfigs.some(({ | |
| 54 | config: { | |
| 55 | options: { | |
| 56 | ignore, | |
| 57 | only | |
| 58 | } | |
| 59 | } | |
| 60 | }) => shouldIgnore(context, ignore, only, dirname))) { | |
| 61 | return null; | |
| 62 | } | |
| 63 | const chain = emptyChain(); | |
| 64 | const logger = createLogger(input, context, baseLogger); | |
| 65 | for (const { | |
| 66 | config, | |
| 67 | index, | |
| 68 | envName | |
| 69 | } of flattenedConfigs) { | |
| 70 | if (!(yield* mergeExtendsChain(chain, config.options, dirname, context, files, baseLogger))) { | |
| 71 | return null; | |
| 72 | } | |
| 73 | logger(config, index, envName); | |
| 74 | yield* mergeChainOpts(chain, config); | |
| 75 | } | |
| 76 | return chain; | |
| 77 | } | |
| 78 | ||
| 79 | // buildRootChain | |
| 80 | function* buildRootChain(opts, context) { | |
| 81 | let configReport, babelRcReport; | |
| 82 | const programmaticLogger = new _printer.ConfigPrinter(); | |
| 83 | const programmaticChain = yield* loadProgrammaticChain({ | |
| 84 | options: opts, | |
| 85 | dirname: context.cwd | |
| 86 | }, context, undefined, programmaticLogger); | |
| 87 | if (!programmaticChain) return null; | |
| 88 | const programmaticReport = yield* programmaticLogger.output(); | |
| 89 | let configFile; | |
| 90 | if (typeof opts.configFile === "string") { | |
| 91 | configFile = yield* (0, _index.loadConfig)(opts.configFile, context.cwd, context.envName, context.caller); | |
| 92 | } else if (opts.configFile !== false) { | |
| 93 | configFile = yield* (0, _index.findRootConfig)(context.root, context.envName, context.caller); | |
| 94 | } | |
| 95 | let { | |
| 96 | babelrc, | |
| 97 | babelrcRoots | |
| 98 | } = opts; | |
| 99 | let babelrcRootsDirectory = context.cwd; | |
| 100 | const configFileChain = emptyChain(); | |
| 101 | const configFileLogger = new _printer.ConfigPrinter(); | |
| 102 | if (configFile) { | |
| 103 | const validatedFile = validateConfigFile(configFile); | |
| 104 | const result = yield* loadFileChain(validatedFile, context, undefined, configFileLogger); | |
| 105 | if (!result) return null; | |
| 106 | configReport = yield* configFileLogger.output(); | |
| 107 | if (babelrc === undefined) { | |
| 108 | babelrc = validatedFile.options.babelrc; | |
| 109 | } | |
| 110 | if (babelrcRoots === undefined) { | |
| 111 | babelrcRootsDirectory = validatedFile.dirname; | |
| 112 | babelrcRoots = validatedFile.options.babelrcRoots; | |
| 113 | } | |
| 114 | mergeChain(configFileChain, result); | |
| 115 | } | |
| 116 | let ignoreFile, babelrcFile; | |
| 117 | let isIgnored = false; | |
| 118 | const fileChain = emptyChain(); | |
| 119 | if ((babelrc === true || babelrc === undefined) && typeof context.filename === "string") { | |
| 120 | const pkgData = yield* (0, _index.findPackageData)(context.filename); | |
| 121 | if (pkgData && babelrcLoadEnabled(context, pkgData, babelrcRoots, babelrcRootsDirectory)) { | |
| 122 | ({ | |
| 123 | ignore: ignoreFile, | |
| 124 | config: babelrcFile | |
| 125 | } = yield* (0, _index.findRelativeConfig)(pkgData, context.envName, context.caller)); | |
| 126 | if (ignoreFile) { | |
| 127 | fileChain.files.add(ignoreFile.filepath); | |
| 128 | } | |
| 129 | if (ignoreFile && shouldIgnore(context, ignoreFile.ignore, null, ignoreFile.dirname)) { | |
| 130 | isIgnored = true; | |
| 131 | } | |
| 132 | if (babelrcFile && !isIgnored) { | |
| 133 | const validatedFile = validateBabelrcFile(babelrcFile); | |
| 134 | const babelrcLogger = new _printer.ConfigPrinter(); | |
| 135 | const result = yield* loadFileChain(validatedFile, context, undefined, babelrcLogger); | |
| 136 | if (!result) { | |
| 137 | isIgnored = true; | |
| 138 | } else { | |
| 139 | babelRcReport = yield* babelrcLogger.output(); | |
| 140 | mergeChain(fileChain, result); | |
| 141 | } | |
| 142 | } | |
| 143 | if (babelrcFile && isIgnored) { | |
| 144 | fileChain.files.add(babelrcFile.filepath); | |
| 145 | } | |
| 146 | } | |
| 147 | } | |
| 148 | if (context.showConfig) { | |
| 149 | console.log(`Babel configs on "${context.filename}" (ascending priority):\n` + [configReport, babelRcReport, programmaticReport].filter(x => !!x).join("\n\n") + "\n-----End Babel configs-----"); | |
| 150 | } | |
| 151 | const chain = mergeChain(mergeChain(mergeChain(emptyChain(), configFileChain), fileChain), programmaticChain); | |
| 152 | return { | |
| 153 | plugins: isIgnored ? [] : dedupDescriptors(chain.plugins), | |
| 154 | presets: isIgnored ? [] : dedupDescriptors(chain.presets), | |
| 155 | options: isIgnored ? [] : chain.options.map(o => normalizeOptions(o)), | |
| 156 | fileHandling: isIgnored ? "ignored" : "transpile", | |
| 157 | ignore: ignoreFile || undefined, | |
| 158 | babelrc: babelrcFile || undefined, | |
| 159 | config: configFile || undefined, | |
| 160 | files: chain.files | |
| 161 | }; | |
| 162 | } |
src/file-viewer/static/captcha/image/4.jpeg created+26| ... | ... | @@ -0,0 +1,26 @@ |
| 1 | // assumptionsNames | |
| 2 | [object Set] | |
| 3 | ||
| 4 | // checkNoUnwrappedItemOptionPairs | |
| 5 | function checkNoUnwrappedItemOptionPairs(items, index, type, e) { | |
| 6 | if (index === 0) return; | |
| 7 | const lastItem = items[index - 1]; | |
| 8 | const thisItem = items[index]; | |
| 9 | if (lastItem.file && lastItem.options === undefined && typeof thisItem.value === "object") { | |
| 10 | e.message += `\n- Maybe you meant to use\n` + `"${type}s": [\n ["${lastItem.file.request}", ${JSON.stringify(thisItem.value, undefined, 2)}]\n]\n` + `To be a valid ${type}, its name and options should be wrapped in a pair of brackets`; | |
| 11 | } | |
| 12 | } | |
| 13 | ||
| 14 | // validate | |
| 15 | function validate(type, opts, filename) { | |
| 16 | try { | |
| 17 | return validateNested({ | |
| 18 | type: "root", | |
| 19 | source: type | |
| 20 | }, opts); | |
| 21 | } catch (error) { | |
| 22 | const configError = new _configError.default(error.message, filename); | |
| 23 | if (error.code) configError.code = error.code; | |
| 24 | throw configError; | |
| 25 | } | |
| 26 | } |
src/file-viewer/static/captcha/image/5.jpeg created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | // default | |
| 2 | [object Object] |
src/file-viewer/static/captcha/image/6.jpeg created+229| ... | ... | @@ -0,0 +1,229 @@ |
| 1 | // access | |
| 2 | function access(loc, name) { | |
| 3 | return { | |
| 4 | type: "access", | |
| 5 | name, | |
| 6 | parent: loc | |
| 7 | }; | |
| 8 | } | |
| 9 | ||
| 10 | // assertArray | |
| 11 | function assertArray(loc, value) { | |
| 12 | if (value != null && !Array.isArray(value)) { | |
| 13 | throw new Error(`${msg(loc)} must be an array, or undefined`); | |
| 14 | } | |
| 15 | return value; | |
| 16 | } | |
| 17 | ||
| 18 | // assertAssumptions | |
| 19 | function assertAssumptions(loc, value) { | |
| 20 | if (value === undefined) return; | |
| 21 | if (typeof value !== "object" || value === null) { | |
| 22 | throw new Error(`${msg(loc)} must be an object or undefined.`); | |
| 23 | } | |
| 24 | let root = loc; | |
| 25 | do { | |
| 26 | root = root.parent; | |
| 27 | } while (root.type !== "root"); | |
| 28 | const inPreset = root.source === "preset"; | |
| 29 | for (const name of Object.keys(value)) { | |
| 30 | const subLoc = access(loc, name); | |
| 31 | if (!_options.assumptionsNames.has(name)) { | |
| 32 | throw new Error(`${msg(subLoc)} is not a supported assumption.`); | |
| 33 | } | |
| 34 | if (typeof value[name] !== "boolean") { | |
| 35 | throw new Error(`${msg(subLoc)} must be a boolean.`); | |
| 36 | } | |
| 37 | if (inPreset && value[name] === false) { | |
| 38 | throw new Error(`${msg(subLoc)} cannot be set to 'false' inside presets.`); | |
| 39 | } | |
| 40 | } | |
| 41 | return value; | |
| 42 | } | |
| 43 | ||
| 44 | // assertBabelrcSearch | |
| 45 | function assertBabelrcSearch(loc, value) { | |
| 46 | if (value === undefined || typeof value === "boolean") { | |
| 47 | return value; | |
| 48 | } | |
| 49 | if (Array.isArray(value)) { | |
| 50 | value.forEach((item, i) => { | |
| 51 | if (!checkValidTest(item)) { | |
| 52 | throw new Error(`${msg(access(loc, i))} must be a string/Function/RegExp.`); | |
| 53 | } | |
| 54 | }); | |
| 55 | } else if (!checkValidTest(value)) { | |
| 56 | throw new Error(`${msg(loc)} must be a undefined, a boolean, a string/Function/RegExp ` + `or an array of those, got ${JSON.stringify(value)}`); | |
| 57 | } | |
| 58 | return value; | |
| 59 | } | |
| 60 | ||
| 61 | // assertBoolean | |
| 62 | function assertBoolean(loc, value) { | |
| 63 | if (value !== undefined && typeof value !== "boolean") { | |
| 64 | throw new Error(`${msg(loc)} must be a boolean, or undefined`); | |
| 65 | } | |
| 66 | return value; | |
| 67 | } | |
| 68 | ||
| 69 | // assertCallerMetadata | |
| 70 | function assertCallerMetadata(loc, value) { | |
| 71 | const obj = assertObject(loc, value); | |
| 72 | if (obj) { | |
| 73 | if (typeof obj.name !== "string") { | |
| 74 | throw new Error(`${msg(loc)} set but does not contain "name" property string`); | |
| 75 | } | |
| 76 | for (const prop of Object.keys(obj)) { | |
| 77 | const propLoc = access(loc, prop); | |
| 78 | const value = obj[prop]; | |
| 79 | if (value != null && typeof value !== "boolean" && typeof value !== "string" && typeof value !== "number") { | |
| 80 | throw new Error(`${msg(propLoc)} must be null, undefined, a boolean, a string, or a number.`); | |
| 81 | } | |
| 82 | } | |
| 83 | } | |
| 84 | return value; | |
| 85 | } | |
| 86 | ||
| 87 | // assertCompact | |
| 88 | function assertCompact(loc, value) { | |
| 89 | if (value !== undefined && typeof value !== "boolean" && value !== "auto") { | |
| 90 | throw new Error(`${msg(loc)} must be a boolean, "auto", or undefined`); | |
| 91 | } | |
| 92 | return value; | |
| 93 | } | |
| 94 | ||
| 95 | // assertConfigApplicableTest | |
| 96 | function assertConfigApplicableTest(loc, value) { | |
| 97 | if (value === undefined) { | |
| 98 | return value; | |
| 99 | } | |
| 100 | if (Array.isArray(value)) { | |
| 101 | value.forEach((item, i) => { | |
| 102 | if (!checkValidTest(item)) { | |
| 103 | throw new Error(`${msg(access(loc, i))} must be a string/Function/RegExp.`); | |
| 104 | } | |
| 105 | }); | |
| 106 | } else if (!checkValidTest(value)) { | |
| 107 | throw new Error(`${msg(loc)} must be a string/Function/RegExp, or an array of those`); | |
| 108 | } | |
| 109 | return value; | |
| 110 | } | |
| 111 | ||
| 112 | // assertConfigFileSearch | |
| 113 | function assertConfigFileSearch(loc, value) { | |
| 114 | if (value !== undefined && typeof value !== "boolean" && typeof value !== "string") { | |
| 115 | throw new Error(`${msg(loc)} must be a undefined, a boolean, a string, ` + `got ${JSON.stringify(value)}`); | |
| 116 | } | |
| 117 | return value; | |
| 118 | } | |
| 119 | ||
| 120 | // assertFunction | |
| 121 | function assertFunction(loc, value) { | |
| 122 | if (value !== undefined && typeof value !== "function") { | |
| 123 | throw new Error(`${msg(loc)} must be a function, or undefined`); | |
| 124 | } | |
| 125 | return value; | |
| 126 | } | |
| 127 | ||
| 128 | // assertIgnoreList | |
| 129 | function assertIgnoreList(loc, value) { | |
| 130 | const arr = assertArray(loc, value); | |
| 131 | arr == null || arr.forEach((item, i) => assertIgnoreItem(access(loc, i), item)); | |
| 132 | return arr; | |
| 133 | } | |
| 134 | ||
| 135 | // assertInputSourceMap | |
| 136 | function assertInputSourceMap(loc, value) { | |
| 137 | if (value !== undefined && typeof value !== "boolean" && (typeof value !== "object" || !value)) { | |
| 138 | throw new Error(`${msg(loc)} must be a boolean, object, or undefined`); | |
| 139 | } | |
| 140 | return value; | |
| 141 | } | |
| 142 | ||
| 143 | // assertObject | |
| 144 | function assertObject(loc, value) { | |
| 145 | if (value !== undefined && (typeof value !== "object" || Array.isArray(value) || !value)) { | |
| 146 | throw new Error(`${msg(loc)} must be an object, or undefined`); | |
| 147 | } | |
| 148 | return value; | |
| 149 | } | |
| 150 | ||
| 151 | // assertPluginList | |
| 152 | function assertPluginList(loc, value) { | |
| 153 | const arr = assertArray(loc, value); | |
| 154 | if (arr) { | |
| 155 | arr.forEach((item, i) => assertPluginItem(access(loc, i), item)); | |
| 156 | } | |
| 157 | return arr; | |
| 158 | } | |
| 159 | ||
| 160 | // assertRootMode | |
| 161 | function assertRootMode(loc, value) { | |
| 162 | if (value !== undefined && value !== "root" && value !== "upward" && value !== "upward-optional") { | |
| 163 | throw new Error(`${msg(loc)} must be a "root", "upward", "upward-optional" or undefined`); | |
| 164 | } | |
| 165 | return value; | |
| 166 | } | |
| 167 | ||
| 168 | // assertSourceMaps | |
| 169 | function assertSourceMaps(loc, value) { | |
| 170 | if (value !== undefined && typeof value !== "boolean" && value !== "inline" && value !== "both") { | |
| 171 | throw new Error(`${msg(loc)} must be a boolean, "inline", "both", or undefined`); | |
| 172 | } | |
| 173 | return value; | |
| 174 | } | |
| 175 | ||
| 176 | // assertSourceType | |
| 177 | function assertSourceType(loc, value) { | |
| 178 | if (value !== undefined && value !== "module" && value !== "script" && value !== "unambiguous") { | |
| 179 | throw new Error(`${msg(loc)} must be "module", "script", "unambiguous", or undefined`); | |
| 180 | } | |
| 181 | return value; | |
| 182 | } | |
| 183 | ||
| 184 | // assertString | |
| 185 | function assertString(loc, value) { | |
| 186 | if (value !== undefined && typeof value !== "string") { | |
| 187 | throw new Error(`${msg(loc)} must be a string, or undefined`); | |
| 188 | } | |
| 189 | return value; | |
| 190 | } | |
| 191 | ||
| 192 | // assertTargets | |
| 193 | function assertTargets(loc, value) { | |
| 194 | if ((0, _helperCompilationTargets().isBrowsersQueryValid)(value)) return value; | |
| 195 | if (typeof value !== "object" || !value || Array.isArray(value)) { | |
| 196 | throw new Error(`${msg(loc)} must be a string, an array of strings or an object`); | |
| 197 | } | |
| 198 | const browsersLoc = access(loc, "browsers"); | |
| 199 | const esmodulesLoc = access(loc, "esmodules"); | |
| 200 | assertBrowsersList(browsersLoc, value.browsers); | |
| 201 | assertBoolean(esmodulesLoc, value.esmodules); | |
| 202 | for (const key of Object.keys(value)) { | |
| 203 | const val = value[key]; | |
| 204 | const subLoc = access(loc, key); | |
| 205 | if (key === "esmodules") assertBoolean(subLoc, val);else if (key === "browsers") assertBrowsersList(subLoc, val);else if (!hasOwnProperty.call(_helperCompilationTargets().TargetNames, key)) { | |
| 206 | const validTargets = Object.keys(_helperCompilationTargets().TargetNames).join(", "); | |
| 207 | throw new Error(`${msg(subLoc)} is not a valid target. Supported targets are ${validTargets}`); | |
| 208 | } else assertBrowserVersion(subLoc, val); | |
| 209 | } | |
| 210 | return value; | |
| 211 | } | |
| 212 | ||
| 213 | // msg | |
| 214 | function msg(loc) { | |
| 215 | switch (loc.type) { | |
| 216 | case "root": | |
| 217 | return ``; | |
| 218 | case "env": | |
| 219 | return `${msg(loc.parent)}.env["${loc.name}"]`; | |
| 220 | case "overrides": | |
| 221 | return `${msg(loc.parent)}.overrides[${loc.index}]`; | |
| 222 | case "option": | |
| 223 | return `${msg(loc.parent)}.${loc.name}`; | |
| 224 | case "access": | |
| 225 | return `${msg(loc.parent)}[${JSON.stringify(loc.name)}]`; | |
| 226 | default: | |
| 227 | throw new Error(`Assertion failure: Unknown type ${loc.type}`); | |
| 228 | } | |
| 229 | } |
src/file-viewer/static/captcha/image/7.jpeg created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | // default | |
| 2 | function pathToPattern(pattern, dirname) { | |
| 3 | const parts = _path().resolve(dirname, pattern).split(_path().sep); | |
| 4 | return new RegExp(["^", ...parts.map((part, i) => { | |
| 5 | const last = i === parts.length - 1; | |
| 6 | if (part === "**") return last ? starStarPatLast : starStarPat; | |
| 7 | if (part === "*") return last ? starPatLast : starPat; | |
| 8 | if (part.indexOf("*.") === 0) { | |
| 9 | return substitution + escapeRegExp(part.slice(1)) + (last ? endSep : sep); | |
| 10 | } | |
| 11 | return escapeRegExp(part) + (last ? endSep : sep); | |
| 12 | })].join("")); | |
| 13 | } |
src/file-viewer/static/captcha/image/8.jpeg created+37| ... | ... | @@ -0,0 +1,37 @@ |
| 1 | // ChainFormatter | |
| 2 | [object Object] | |
| 3 | ||
| 4 | // ConfigPrinter | |
| 5 | class ConfigPrinter { | |
| 6 | constructor() { | |
| 7 | this._stack = []; | |
| 8 | } | |
| 9 | configure(enabled, type, { | |
| 10 | callerName, | |
| 11 | filepath | |
| 12 | }) { | |
| 13 | if (!enabled) return () => {}; | |
| 14 | return (content, index, envName) => { | |
| 15 | this._stack.push({ | |
| 16 | type, | |
| 17 | callerName, | |
| 18 | filepath, | |
| 19 | content, | |
| 20 | index, | |
| 21 | envName | |
| 22 | }); | |
| 23 | }; | |
| 24 | } | |
| 25 | static *format(config) { | |
| 26 | let title = Formatter.title(config.type, config.callerName, config.filepath); | |
| 27 | const loc = Formatter.loc(config.index, config.envName); | |
| 28 | if (loc) title += ` ${loc}`; | |
| 29 | const content = yield* Formatter.optionsAndDescriptors(config.content); | |
| 30 | return `${title}\n${content}`; | |
| 31 | } | |
| 32 | *output() { | |
| 33 | if (this._stack.length === 0) return ""; | |
| 34 | const configs = yield* _gensync().all(this._stack.map(s => ConfigPrinter.format(s))); | |
| 35 | return configs.join("\n\n"); | |
| 36 | } | |
| 37 | } |
src/file-viewer/static/captcha/image/9.jpeg created+23| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | // validatePluginObject | |
| 2 | function validatePluginObject(obj) { | |
| 3 | const rootPath = { | |
| 4 | type: "root", | |
| 5 | source: "plugin" | |
| 6 | }; | |
| 7 | Object.keys(obj).forEach(key => { | |
| 8 | const validator = VALIDATORS[key]; | |
| 9 | if (validator) { | |
| 10 | const optLoc = { | |
| 11 | type: "option", | |
| 12 | name: key, | |
| 13 | parent: rootPath | |
| 14 | }; | |
| 15 | validator(optLoc, obj[key]); | |
| 16 | } else { | |
| 17 | const invalidPluginPropertyError = new Error(`.${key} is not a valid Plugin property`); | |
| 18 | invalidPluginPropertyError.code = "BABEL_UNKNOWN_PLUGIN_PROPERTY"; | |
| 19 | throw invalidPluginPropertyError; | |
| 20 | } | |
| 21 | }); | |
| 22 | return obj; | |
| 23 | } |
src/file-viewer/test-tree.ts created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | // getEnv | |
| 2 | function getEnv(defaultValue = "development") { | |
| 3 | return process.env.BABEL_ENV || process.env.NODE_ENV || defaultValue; | |
| 4 | } |
src/file-viewer/text-formatting.ts created+1073| ... | ... | @@ -0,0 +1,1073 @@ |
| 1 | // __esModule | |
| 2 | true | |
| 3 | ||
| 4 | // DiagnosticType | |
| 5 | [object Object] | |
| 6 | ||
| 7 | // assertAllowedAttributes | |
| 8 | function assertAllowedAttributes(path, allowed) { | |
| 9 | let i = 0; | |
| 10 | for (const attr of path.node.attributes) { | |
| 11 | if (attr.type === "MarkoSpreadAttribute") { | |
| 12 | throw path.hub.buildError( | |
| 13 | attr, | |
| 14 | `Tag does not support spread attributes.` | |
| 15 | ); | |
| 16 | } else if (!allowed.includes(attr.name)) { | |
| 17 | throw path.hub.buildError( | |
| 18 | attr, | |
| 19 | `Tag does not support the \`${attr.name}\` attribute.` | |
| 20 | ); | |
| 21 | } | |
| 22 | ||
| 23 | i++; | |
| 24 | } | |
| 25 | } | |
| 26 | ||
| 27 | // assertAttributesOrArgs | |
| 28 | function assertAttributesOrArgs(path) { | |
| 29 | const { node } = path; | |
| 30 | const args = node.arguments; | |
| 31 | if (args && args.length && (node.attributes.length > 0 || node.body.length)) { | |
| 32 | const start = args[0].loc.start; | |
| 33 | const end = args[args.length - 1].loc.end; | |
| 34 | throw path.hub.buildError( | |
| 35 | { loc: { start, end } }, | |
| 36 | "Tag does not support arguments when attributes or body present." | |
| 37 | ); | |
| 38 | } | |
| 39 | } | |
| 40 | ||
| 41 | // assertAttributesOrSingleArg | |
| 42 | function assertAttributesOrSingleArg(path) { | |
| 43 | assertAttributesOrArgs(path); | |
| 44 | const args = path.node.arguments; | |
| 45 | if (args && args.length > 1) { | |
| 46 | const start = args[1].loc.start; | |
| 47 | const end = args[args.length - 1].loc.end; | |
| 48 | throw path.hub.buildError( | |
| 49 | { loc: { start, end } }, | |
| 50 | "Tag does not support multiple arguments." | |
| 51 | ); | |
| 52 | } | |
| 53 | } | |
| 54 | ||
| 55 | // assertNoArgs | |
| 56 | function assertNoArgs(path) { | |
| 57 | const args = path.node.arguments; | |
| 58 | if (args && args.length) { | |
| 59 | const start = args[0].loc.start; | |
| 60 | const end = args[args.length - 1].loc.end; | |
| 61 | throw path.hub.buildError( | |
| 62 | { loc: { start, end } }, | |
| 63 | "Tag does not support arguments." | |
| 64 | ); | |
| 65 | } | |
| 66 | } | |
| 67 | ||
| 68 | // assertNoAttributeTags | |
| 69 | function assertNoAttributeTags(path) { | |
| 70 | if (path.node.attributeTags.length) { | |
| 71 | throw path.hub.buildError( | |
| 72 | path.node.attributeTags[0], | |
| 73 | "Tag not support nested attribute tags." | |
| 74 | ); | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | // assertNoAttributes | |
| 79 | function assertNoAttributes(path) { | |
| 80 | const { attributes } = path.node; | |
| 81 | if (attributes.length) { | |
| 82 | const start = attributes[0].loc.start; | |
| 83 | const end = attributes[attributes.length - 1].loc.end; | |
| 84 | throw path.hub.buildError( | |
| 85 | { loc: { start, end } }, | |
| 86 | "Tag does not support attributes." | |
| 87 | ); | |
| 88 | } | |
| 89 | } | |
| 90 | ||
| 91 | // assertNoParams | |
| 92 | function assertNoParams(path) { | |
| 93 | const { params } = path.node.body; | |
| 94 | if (params.length) { | |
| 95 | const start = params[0].loc.start; | |
| 96 | const end = params[params.length - 1].loc.end; | |
| 97 | throw path.hub.buildError( | |
| 98 | { loc: { start, end } }, | |
| 99 | "Tag does not support parameters." | |
| 100 | ); | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | // assertNoVar | |
| 105 | function assertNoVar(path) { | |
| 106 | if (path.node.var) { | |
| 107 | throw path.hub.buildError( | |
| 108 | path.node.var, | |
| 109 | "Tag does not support a variable." | |
| 110 | ); | |
| 111 | } | |
| 112 | } | |
| 113 | ||
| 114 | // computeNode | |
| 115 | function computeNode(node) { | |
| 116 | switch (node.type) { | |
| 117 | case "StringLiteral": | |
| 118 | case "NumericLiteral": | |
| 119 | case "BooleanLiteral": | |
| 120 | return { value: node.value }; | |
| 121 | case "RegExpLiteral": | |
| 122 | return { value: new RegExp(node.pattern, node.flags) }; | |
| 123 | case "NullLiteral": | |
| 124 | return { value: null }; | |
| 125 | case "Identifier": | |
| 126 | switch (node.name) { | |
| 127 | case "undefined": | |
| 128 | return { value: undefined }; | |
| 129 | case "NaN": | |
| 130 | return { value: NaN }; | |
| 131 | case "Infinity": | |
| 132 | return { value: Infinity }; | |
| 133 | default: | |
| 134 | return; | |
| 135 | } | |
| 136 | case "BigIntLiteral": | |
| 137 | return { value: BigInt(node.value) }; | |
| 138 | case "ParenthesizedExpression": | |
| 139 | return computeNode(node.expression); | |
| 140 | case "BinaryExpression":{ | |
| 141 | const left = computeNode(node.left); | |
| 142 | if (!left) return; | |
| 143 | const right = computeNode(node.right); | |
| 144 | if (!right) return; | |
| 145 | switch (node.operator) { | |
| 146 | case "+": | |
| 147 | return { value: left.value + right.value }; | |
| 148 | case "-": | |
| 149 | return { value: left.value - right.value }; | |
| 150 | case "*": | |
| 151 | return { value: left.value * right.value }; | |
| 152 | case "/": | |
| 153 | return { value: left.value / right.value }; | |
| 154 | case "%": | |
| 155 | return { value: left.value % right.value }; | |
| 156 | case "**": | |
| 157 | return { value: left.value ** right.value }; | |
| 158 | case "|": | |
| 159 | return { value: left.value | right.value }; | |
| 160 | case "&": | |
| 161 | return { value: left.value & right.value }; | |
| 162 | case "^": | |
| 163 | return { value: left.value ^ right.value }; | |
| 164 | case "<<": | |
| 165 | return { value: left.value << right.value }; | |
| 166 | case ">>": | |
| 167 | return { value: left.value >> right.value }; | |
| 168 | case ">>>": | |
| 169 | return { value: left.value >>> right.value }; | |
| 170 | case "==": | |
| 171 | return { value: left.value == right.value }; | |
| 172 | case "!=": | |
| 173 | return { value: left.value != right.value }; | |
| 174 | case "===": | |
| 175 | return { value: left.value === right.value }; | |
| 176 | case "!==": | |
| 177 | return { value: left.value !== right.value }; | |
| 178 | case "<": | |
| 179 | return { value: left.value < right.value }; | |
| 180 | case "<=": | |
| 181 | return { value: left.value <= right.value }; | |
| 182 | case ">": | |
| 183 | return { value: left.value > right.value }; | |
| 184 | case ">=": | |
| 185 | return { value: left.value >= right.value }; | |
| 186 | default: | |
| 187 | return; | |
| 188 | } | |
| 189 | } | |
| 190 | case "UnaryExpression":{ | |
| 191 | const arg = computeNode(node.argument); | |
| 192 | if (!arg) return; | |
| 193 | switch (node.operator) { | |
| 194 | case "+": | |
| 195 | return { value: +arg.value }; | |
| 196 | case "-": | |
| 197 | return { value: -arg.value }; | |
| 198 | case "~": | |
| 199 | return { value: ~arg.value }; | |
| 200 | case "!": | |
| 201 | return { value: !arg.value }; | |
| 202 | case "typeof": | |
| 203 | return { value: typeof arg.value }; | |
| 204 | case "void": | |
| 205 | return { value: void arg.value }; | |
| 206 | default: | |
| 207 | return; | |
| 208 | } | |
| 209 | } | |
| 210 | case "LogicalExpression":{ | |
| 211 | const left = computeNode(node.left); | |
| 212 | if (!left) return; | |
| 213 | const right = computeNode(node.right); | |
| 214 | if (!right) return; | |
| 215 | switch (node.operator) { | |
| 216 | case "&&": | |
| 217 | return { value: left.value && right.value }; | |
| 218 | case "||": | |
| 219 | return { value: left.value || right.value }; | |
| 220 | case "??": | |
| 221 | return { value: left.value ?? right.value }; | |
| 222 | default: | |
| 223 | return; | |
| 224 | } | |
| 225 | } | |
| 226 | case "ConditionalExpression":{ | |
| 227 | const test = computeNode(node.test); | |
| 228 | if (!test) return; | |
| 229 | const consequent = computeNode(node.consequent); | |
| 230 | if (!consequent) return; | |
| 231 | const alternate = computeNode(node.alternate); | |
| 232 | if (!alternate) return; | |
| 233 | return { value: test.value ? consequent.value : alternate.value }; | |
| 234 | } | |
| 235 | case "TemplateLiteral":{ | |
| 236 | let value = node.quasis[0].value.cooked; | |
| 237 | for (let i = 0; i < node.expressions.length; i++) { | |
| 238 | const expr = computeNode(node.expressions[i]); | |
| 239 | if (!expr) return; | |
| 240 | value += expr.value + node.quasis[i + 1].value.cooked; | |
| 241 | } | |
| 242 | return { value }; | |
| 243 | } | |
| 244 | case "ObjectExpression":{ | |
| 245 | const value = {}; | |
| 246 | for (const prop of node.properties) { | |
| 247 | if (prop.decorators) return; | |
| 248 | switch (prop.type) { | |
| 249 | case "ObjectProperty":{ | |
| 250 | let key; | |
| 251 | if (prop.computed) { | |
| 252 | const keyNode = computeNode(prop.key); | |
| 253 | if (!keyNode) return; | |
| 254 | key = keyNode.value + ""; | |
| 255 | } else { | |
| 256 | switch (prop.key.type) { | |
| 257 | case "Identifier": | |
| 258 | key = prop.key.name; | |
| 259 | break; | |
| 260 | case "StringLiteral": | |
| 261 | key = prop.key.value; | |
| 262 | break; | |
| 263 | default: | |
| 264 | return; | |
| 265 | } | |
| 266 | } | |
| 267 | ||
| 268 | const propValue = computeNode(prop.value); | |
| 269 | if (!propValue) return; | |
| 270 | value[key] = propValue.value; | |
| 271 | break; | |
| 272 | } | |
| 273 | case "SpreadElement":{ | |
| 274 | const arg = computeNode(prop.argument); | |
| 275 | if (!arg) return; | |
| 276 | Object.assign(value, arg.value); | |
| 277 | break; | |
| 278 | } | |
| 279 | } | |
| 280 | } | |
| 281 | ||
| 282 | return { value }; | |
| 283 | } | |
| 284 | case "ArrayExpression":{ | |
| 285 | const value = []; | |
| 286 | for (const elem of node.elements) { | |
| 287 | if (elem) { | |
| 288 | if (elem.type === "SpreadElement") { | |
| 289 | const arg = computeNode(elem.argument); | |
| 290 | if (typeof arg?.value?.[Symbol.iterator] !== "function") return; | |
| 291 | for (const item of arg.value) { | |
| 292 | value.push(item); | |
| 293 | } | |
| 294 | } else { | |
| 295 | const elemValue = computeNode(elem); | |
| 296 | if (!elemValue) return; | |
| 297 | value.push(elemValue.value); | |
| 298 | } | |
| 299 | } else { | |
| 300 | value.length++; | |
| 301 | } | |
| 302 | } | |
| 303 | ||
| 304 | return { value }; | |
| 305 | } | |
| 306 | } | |
| 307 | } | |
| 308 | ||
| 309 | // defineTag | |
| 310 | function defineTag(tag) { | |
| 311 | return tag; | |
| 312 | } | |
| 313 | ||
| 314 | // diagnosticDeprecate | |
| 315 | function diagnosticDeprecate(path, options) { | |
| 316 | add(DiagnosticType.Deprecation, path, options); | |
| 317 | } | |
| 318 | ||
| 319 | // diagnosticError | |
| 320 | function diagnosticError(path, options) { | |
| 321 | add(DiagnosticType.Error, path, options); | |
| 322 | } | |
| 323 | ||
| 324 | // diagnosticSuggest | |
| 325 | function diagnosticSuggest(path, options) { | |
| 326 | add(DiagnosticType.Suggestion, path, options); | |
| 327 | } | |
| 328 | ||
| 329 | // diagnosticWarn | |
| 330 | function diagnosticWarn(path, options) { | |
| 331 | add(DiagnosticType.Warning, path, options); | |
| 332 | } | |
| 333 | ||
| 334 | // findAttributeTags | |
| 335 | function findAttributeTags(path, attributeTags = []) { | |
| 336 | const attrTags = path.node.body.attributeTags ? | |
| 337 | path.get("body").get("body") : | |
| 338 | path.get("attributeTags"); | |
| 339 | attrTags.forEach((child) => { | |
| 340 | if (isAttributeTag(child)) { | |
| 341 | attributeTags.push(child); | |
| 342 | } else if (isTransparentTag(child)) { | |
| 343 | findAttributeTags(child, attributeTags); | |
| 344 | } | |
| 345 | }); | |
| 346 | ||
| 347 | return attributeTags; | |
| 348 | } | |
| 349 | ||
| 350 | // findParentTag | |
| 351 | function findParentTag(path) { | |
| 352 | let cur = path.parentPath; | |
| 353 | ||
| 354 | while (cur.node) { | |
| 355 | if (cur.isMarkoTagBody()) { | |
| 356 | cur = cur.parentPath; | |
| 357 | continue; | |
| 358 | } | |
| 359 | ||
| 360 | if (!cur.isMarkoTag()) { | |
| 361 | cur = undefined; | |
| 362 | break; | |
| 363 | } | |
| 364 | ||
| 365 | if (isTransparentTag(cur)) { | |
| 366 | cur = cur.parentPath; | |
| 367 | continue; | |
| 368 | } | |
| 369 | ||
| 370 | return cur; | |
| 371 | } | |
| 372 | } | |
| 373 | ||
| 374 | // getArgOrSequence | |
| 375 | function getArgOrSequence(path) { | |
| 376 | const { | |
| 377 | node: { arguments: args } | |
| 378 | } = path; | |
| 379 | const len = args && args.length; | |
| 380 | ||
| 381 | if (len) { | |
| 382 | if (len > 1) { | |
| 383 | return _compiler.types.sequenceExpression(args); | |
| 384 | } else { | |
| 385 | return args[0]; | |
| 386 | } | |
| 387 | } | |
| 388 | } | |
| 389 | ||
| 390 | // getEnd | |
| 391 | function getEnd(file, node) { | |
| 392 | // Restore if merged: https://github.com/babel/babel/pull/16849 | |
| 393 | // if (node.end != null) { | |
| 394 | // return node.end; | |
| 395 | // } | |
| 396 | ||
| 397 | if (node.loc) { | |
| 398 | return locToIndex(file, node.loc.end); | |
| 399 | } | |
| 400 | ||
| 401 | return null; | |
| 402 | } | |
| 403 | ||
| 404 | // getFile | |
| 405 | function getFile() { | |
| 406 | if (currentFile) { | |
| 407 | return currentFile; | |
| 408 | } | |
| 409 | ||
| 410 | throw new Error("Unable to access Marko File outside of a compilation"); | |
| 411 | } | |
| 412 | ||
| 413 | // getFullyResolvedTagName | |
| 414 | function getFullyResolvedTagName(path) { | |
| 415 | const parts = []; | |
| 416 | let cur; | |
| 417 | do { | |
| 418 | cur = path.node.name.value; | |
| 419 | ||
| 420 | if (isAttributeTag(path)) { | |
| 421 | parts.push(cur.slice(1)); | |
| 422 | } else { | |
| 423 | parts.push(cur || "*"); | |
| 424 | break; | |
| 425 | } | |
| 426 | } while (path = findParentTag(path)); | |
| 427 | ||
| 428 | return parts.reverse().join(":"); | |
| 429 | } | |
| 430 | ||
| 431 | // getLoc | |
| 432 | function getLoc(file, index) { | |
| 433 | return findLoc(getLineIndexes(file), 0, index); | |
| 434 | } | |
| 435 | ||
| 436 | // getLocRange | |
| 437 | function getLocRange(file, start, end) { | |
| 438 | const lineIndexes = getLineIndexes(file); | |
| 439 | const startLoc = findLoc(lineIndexes, 0, start); | |
| 440 | ||
| 441 | if (startLoc) { | |
| 442 | const endLoc = | |
| 443 | start === end ? startLoc : findLoc(lineIndexes, startLoc.line - 1, end); | |
| 444 | ||
| 445 | return { | |
| 446 | start: startLoc, | |
| 447 | end: endLoc | |
| 448 | }; | |
| 449 | } | |
| 450 | } | |
| 451 | ||
| 452 | // getMacroIdentifier | |
| 453 | function getMacroIdentifier(path) { | |
| 454 | const { file } = path.hub; | |
| 455 | ||
| 456 | if (file.___compileStage !== "translate") { | |
| 457 | throw new Error( | |
| 458 | "getMacroIdentifier can only be called during the translate phase of the compiler." | |
| 459 | ); | |
| 460 | } | |
| 461 | ||
| 462 | if (!isMacroTag(path)) { | |
| 463 | throw path.buildCodeFrameError( | |
| 464 | "getMacroIdentifier called on non macro referencing tag." | |
| 465 | ); | |
| 466 | } | |
| 467 | ||
| 468 | return getMacroIdentifierForName(path, path.node.name.value); | |
| 469 | } | |
| 470 | ||
| 471 | // getMacroIdentifierForName | |
| 472 | function getMacroIdentifierForName(path, name) { | |
| 473 | const { file } = path.hub; | |
| 474 | ||
| 475 | if (file.___compileStage !== "translate") { | |
| 476 | throw new Error( | |
| 477 | "getMacroIdentifierForName can only be called during the translate phase of the compiler." | |
| 478 | ); | |
| 479 | } | |
| 480 | ||
| 481 | const markoMeta = file.metadata.marko; | |
| 482 | let macroIds = markoMeta[MACRO_IDS_KEY]; | |
| 483 | ||
| 484 | if (!macroIds) { | |
| 485 | macroIds = markoMeta[MACRO_IDS_KEY] = {}; | |
| 486 | ||
| 487 | for (const macroName in markoMeta[MACRO_NAMES_KEY]) { | |
| 488 | macroIds[macroName] = file.path.scope.generateUid(macroName); | |
| 489 | } | |
| 490 | } | |
| 491 | ||
| 492 | const id = macroIds[name]; | |
| 493 | ||
| 494 | if (!id) { | |
| 495 | throw new Error( | |
| 496 | "<macro> was added programmatically, but was not registered via the 'registerMacro' api in @marko/compiler/babel-utils." | |
| 497 | ); | |
| 498 | } | |
| 499 | ||
| 500 | return _compiler.types.identifier(id); | |
| 501 | } | |
| 502 | ||
| 503 | // getProgram | |
| 504 | function getProgram() { | |
| 505 | if (currentFile) { | |
| 506 | return currentFile.path; | |
| 507 | } | |
| 508 | ||
| 509 | throw new Error("Unable to access Marko Program outside of a compilation"); | |
| 510 | } | |
| 511 | ||
| 512 | // getStart | |
| 513 | function getStart(file, node) { | |
| 514 | // Restore if merged: https://github.com/babel/babel/pull/16849 | |
| 515 | // if (node.start != null) { | |
| 516 | // return node.start; | |
| 517 | // } | |
| 518 | ||
| 519 | if (node.loc) { | |
| 520 | return locToIndex(file, node.loc.start); | |
| 521 | } | |
| 522 | ||
| 523 | return null; | |
| 524 | } | |
| 525 | ||
| 526 | // getTagDef | |
| 527 | function getTagDef(path) { | |
| 528 | const { | |
| 529 | node, | |
| 530 | hub: { file } | |
| 531 | } = path; | |
| 532 | ||
| 533 | if (node.tagDef === undefined) { | |
| 534 | if (isDynamicTag(path) || isMacroTag(path)) { | |
| 535 | node.tagDef = null; | |
| 536 | } else { | |
| 537 | node.tagDef = | |
| 538 | (0, _taglib.getTagDefForTagName)( | |
| 539 | file, | |
| 540 | isAttributeTag(path) ? | |
| 541 | getFullyResolvedTagName(path) : | |
| 542 | node.name.value | |
| 543 | ) || null; | |
| 544 | } | |
| 545 | } | |
| 546 | ||
| 547 | return node.tagDef; | |
| 548 | } | |
| 549 | ||
| 550 | // getTagDefForTagName | |
| 551 | function getTagDefForTagName(file, tagName) { | |
| 552 | const tagDef = getTaglibLookup(file).getTag(tagName); | |
| 553 | ||
| 554 | if (tagDef) { | |
| 555 | let seen = file.metadata.marko[SEEN_TAGS_KEY]; | |
| 556 | if (!seen) { | |
| 557 | seen = file.metadata.marko[SEEN_TAGS_KEY] = new Set(); | |
| 558 | } | |
| 559 | ||
| 560 | if (!seen.has(tagDef)) { | |
| 561 | seen.add(tagName); | |
| 562 | const { filePath } = tagDef; | |
| 563 | const len = filePath.length; | |
| 564 | ||
| 565 | if (filePath[len - 14] === "m" && filePath.endsWith("marko-tag.json")) { | |
| 566 | file.metadata.marko.watchFiles.push(filePath); | |
| 567 | } | |
| 568 | } | |
| 569 | } | |
| 570 | return tagDef; | |
| 571 | } | |
| 572 | ||
| 573 | // getTagTemplate | |
| 574 | function getTagTemplate(tag) { | |
| 575 | const { | |
| 576 | node, | |
| 577 | hub: { file } | |
| 578 | } = tag; | |
| 579 | ||
| 580 | if (node.extra?.tagNameImported) { | |
| 581 | return (0, _path.join)(file.opts.filename, node.extra.tagNameImported); | |
| 582 | } | |
| 583 | return getTagDef(tag)?.template; | |
| 584 | } | |
| 585 | ||
| 586 | // getTaglibLookup | |
| 587 | function getTaglibLookup(file) { | |
| 588 | return file.___taglibLookup; | |
| 589 | } | |
| 590 | ||
| 591 | // getTemplateId | |
| 592 | function getTemplateId(opts, request, child) { | |
| 593 | const id = (0, _path.relative)(ROOT, request).replace(/[^a-zA-Z0-9_$./-]/g, "/"); | |
| 594 | const optimize = typeof opts === "object" ? opts.optimize : opts; | |
| 595 | ||
| 596 | if (optimize) { | |
| 597 | const optimizeKnownTemplates = | |
| 598 | typeof opts === "object" && opts.optimizeKnownTemplates; | |
| 599 | const knownTemplatesSize = optimizeKnownTemplates?.length || 0; | |
| 600 | if (knownTemplatesSize) { | |
| 601 | let lookup = idCache.get(optimizeKnownTemplates); | |
| 602 | if (!lookup) { | |
| 603 | lookup = new Map(); | |
| 604 | idCache.set(optimizeKnownTemplates, lookup); | |
| 605 | for (let i = 0; i < knownTemplatesSize; i++) { | |
| 606 | lookup.set(optimizeKnownTemplates[i], { | |
| 607 | id: encodeTemplateId(i), | |
| 608 | children: new Map() | |
| 609 | }); | |
| 610 | } | |
| 611 | } | |
| 612 | let registered = lookup.get(request); | |
| 613 | if (registered) { | |
| 614 | if (child) { | |
| 615 | let childId = registered.children.get(child); | |
| 616 | if (childId === undefined) { | |
| 617 | childId = registered.children.size; | |
| 618 | registered.children.set(child, childId); | |
| 619 | } | |
| 620 | return registered.id + childId; | |
| 621 | } | |
| 622 | return registered.id; | |
| 623 | } | |
| 624 | } | |
| 625 | ||
| 626 | const hash = (0, _crypto.createHash)("shake256", templateIdHashOpts).update(id); | |
| 627 | if (child) { | |
| 628 | hash.update(child); | |
| 629 | } | |
| 630 | ||
| 631 | return encodeTemplateId(parseInt(hash.digest("hex"), 16)); | |
| 632 | } | |
| 633 | ||
| 634 | return id + (child ? `_${child}` : ""); | |
| 635 | } | |
| 636 | ||
| 637 | // hasMacro | |
| 638 | function hasMacro(path, name) { | |
| 639 | const macroNames = path.hub.file.metadata.marko[MACRO_NAMES_KEY]; | |
| 640 | return !!(macroNames && macroNames[name]); | |
| 641 | } | |
| 642 | ||
| 643 | // importDefault | |
| 644 | function importDefault(file, request, nameHint) { | |
| 645 | const imports = getImports(file); | |
| 646 | request = resolveRelativePath(file, request); | |
| 647 | let importDeclaration = imports.get(request); | |
| 648 | ||
| 649 | if (!importDeclaration) { | |
| 650 | imports.set( | |
| 651 | request, | |
| 652 | importDeclaration = file.path.pushContainer( | |
| 653 | "body", | |
| 654 | _compiler.types.importDeclaration([], _compiler.types.stringLiteral(request)) | |
| 655 | )[0] | |
| 656 | ); | |
| 657 | } | |
| 658 | ||
| 659 | if (!nameHint) { | |
| 660 | return; | |
| 661 | } | |
| 662 | ||
| 663 | const specifiers = importDeclaration.get("specifiers"); | |
| 664 | const specifier = specifiers.find((specifier) => | |
| 665 | specifier.isImportDefaultSpecifier() | |
| 666 | ); | |
| 667 | ||
| 668 | if (!specifier) { | |
| 669 | const identifier = file.scope.generateUidIdentifier(nameHint); | |
| 670 | importDeclaration.pushContainer( | |
| 671 | "specifiers", | |
| 672 | _compiler.types.importDefaultSpecifier(identifier) | |
| 673 | ); | |
| 674 | return identifier; | |
| 675 | } | |
| 676 | ||
| 677 | return _compiler.types.identifier(specifier.node.local.name); | |
| 678 | } | |
| 679 | ||
| 680 | // importNamed | |
| 681 | function importNamed(file, request, name, nameHint = name) { | |
| 682 | request = resolveRelativePath(file, request); | |
| 683 | const imports = getImports(file); | |
| 684 | let importDeclaration = imports.get(request); | |
| 685 | ||
| 686 | if (!importDeclaration) { | |
| 687 | imports.set( | |
| 688 | request, | |
| 689 | importDeclaration = file.path.pushContainer( | |
| 690 | "body", | |
| 691 | _compiler.types.importDeclaration([], _compiler.types.stringLiteral(request)) | |
| 692 | )[0] | |
| 693 | ); | |
| 694 | } | |
| 695 | ||
| 696 | const specifiers = importDeclaration.get("specifiers"); | |
| 697 | const specifier = specifiers.find( | |
| 698 | (specifier) => | |
| 699 | specifier.isImportSpecifier() && specifier.node.imported.name === name | |
| 700 | ); | |
| 701 | ||
| 702 | if (!specifier) { | |
| 703 | const identifier = file.scope.generateUidIdentifier(nameHint); | |
| 704 | importDeclaration.pushContainer( | |
| 705 | "specifiers", | |
| 706 | _compiler.types.importSpecifier(identifier, _compiler.types.identifier(name)) | |
| 707 | ); | |
| 708 | return identifier; | |
| 709 | } | |
| 710 | ||
| 711 | return _compiler.types.identifier(specifier.node.local.name); | |
| 712 | } | |
| 713 | ||
| 714 | // importStar | |
| 715 | function importStar(file, request, nameHint) { | |
| 716 | const imports = getImports(file); | |
| 717 | request = resolveRelativePath(file, request); | |
| 718 | let importDeclaration = imports.get(request); | |
| 719 | ||
| 720 | if (!importDeclaration) { | |
| 721 | imports.set( | |
| 722 | request, | |
| 723 | importDeclaration = file.path.pushContainer( | |
| 724 | "body", | |
| 725 | _compiler.types.importDeclaration([], _compiler.types.stringLiteral(request)) | |
| 726 | )[0] | |
| 727 | ); | |
| 728 | } | |
| 729 | ||
| 730 | if (!nameHint) { | |
| 731 | return; | |
| 732 | } | |
| 733 | ||
| 734 | const specifiers = importDeclaration.get("specifiers"); | |
| 735 | const specifier = specifiers.find((specifier) => | |
| 736 | specifier.isImportNamespaceSpecifier() | |
| 737 | ); | |
| 738 | ||
| 739 | if (!specifier) { | |
| 740 | const identifier = file.scope.generateUidIdentifier(nameHint); | |
| 741 | importDeclaration.pushContainer( | |
| 742 | "specifiers", | |
| 743 | _compiler.types.importNamespaceSpecifier(identifier) | |
| 744 | ); | |
| 745 | return identifier; | |
| 746 | } | |
| 747 | ||
| 748 | return _compiler.types.identifier(specifier.node.local.name); | |
| 749 | } | |
| 750 | ||
| 751 | // isAttributeTag | |
| 752 | function isAttributeTag(path) { | |
| 753 | const { | |
| 754 | node: { name } | |
| 755 | } = path; | |
| 756 | return _compiler.types.isStringLiteral(name) && name.value[0] === "@"; | |
| 757 | } | |
| 758 | ||
| 759 | // isDynamicTag | |
| 760 | function isDynamicTag(path) { | |
| 761 | return !_compiler.types.isStringLiteral(path.node.name); | |
| 762 | } | |
| 763 | ||
| 764 | // isLoopTag | |
| 765 | function isLoopTag(path) { | |
| 766 | if (!path.isMarkoTag()) { | |
| 767 | return false; | |
| 768 | } | |
| 769 | ||
| 770 | const tagName = path.node.name.value; | |
| 771 | return tagName === "while" || tagName === "for"; | |
| 772 | } | |
| 773 | ||
| 774 | // isMacroTag | |
| 775 | function isMacroTag(path) { | |
| 776 | const { name } = path.node; | |
| 777 | return _compiler.types.isStringLiteral(name) && hasMacro(path, name.value); | |
| 778 | } | |
| 779 | ||
| 780 | // isNativeTag | |
| 781 | function isNativeTag(path) { | |
| 782 | if (path.node._isDynamicString) { | |
| 783 | return true; | |
| 784 | } | |
| 785 | ||
| 786 | const tagDef = getTagDef(path); | |
| 787 | return ( | |
| 788 | tagDef && | |
| 789 | tagDef.html && ( | |
| 790 | tagDef.htmlType === "custom-element" || | |
| 791 | !tagDef.template && !tagDef.renderer)); | |
| 792 | ||
| 793 | } | |
| 794 | ||
| 795 | // isTransparentTag | |
| 796 | function isTransparentTag(path) { | |
| 797 | const { | |
| 798 | node: { name } | |
| 799 | } = path; | |
| 800 | return _compiler.types.isStringLiteral(name) && TRANSPARENT_TAGS.has(name.value); | |
| 801 | } | |
| 802 | ||
| 803 | // loadFileForImport | |
| 804 | function loadFileForImport(file, request) { | |
| 805 | const relativeRequest = resolveTagImport(file.path, request); | |
| 806 | ||
| 807 | if (relativeRequest) { | |
| 808 | const filename = | |
| 809 | relativeRequest[0] === "." ? | |
| 810 | (0, _path.resolve)(file.opts.filename, "..", relativeRequest) : | |
| 811 | (0, _resolveFrom.default)((0, _path.dirname)(file.opts.filename), relativeRequest); | |
| 812 | const markoMeta = file.metadata.marko; | |
| 813 | const { analyzedTags } = markoMeta; | |
| 814 | if (analyzedTags) { | |
| 815 | analyzedTags.add(relativeRequest); | |
| 816 | } else { | |
| 817 | markoMeta.analyzedTags = new Set([relativeRequest]); | |
| 818 | } | |
| 819 | ||
| 820 | return resolveMarkoFile(file, filename); | |
| 821 | } | |
| 822 | } | |
| 823 | ||
| 824 | // loadFileForTag | |
| 825 | function loadFileForTag(tag) { | |
| 826 | const { file } = tag.hub; | |
| 827 | if (tag.node.extra?.tagNameImported) { | |
| 828 | return loadFileForImport(file, tag.node.extra?.tagNameImported); | |
| 829 | } | |
| 830 | ||
| 831 | const def = getTagDef(tag); | |
| 832 | const filename = def && def.template; | |
| 833 | ||
| 834 | if (filename) { | |
| 835 | const markoMeta = file.metadata.marko; | |
| 836 | const relativeFileName = (0, _imports.resolveRelativePath)(file, filename); | |
| 837 | const { analyzedTags } = markoMeta; | |
| 838 | if (analyzedTags) { | |
| 839 | analyzedTags.add(relativeFileName); | |
| 840 | } else { | |
| 841 | markoMeta.analyzedTags = new Set([relativeFileName]); | |
| 842 | } | |
| 843 | ||
| 844 | return resolveMarkoFile(file, filename); | |
| 845 | } | |
| 846 | } | |
| 847 | ||
| 848 | // normalizeTemplateString | |
| 849 | function normalizeTemplateString(quasis, ...expressions) { | |
| 850 | quasis = quasis.map((q) => _compiler.types.isTemplateElement(q) ? q.value.cooked : q); | |
| 851 | ||
| 852 | for (let i = expressions.length; i--;) { | |
| 853 | let v = expressions[i]; | |
| 854 | if (_compiler.types.isTemplateLiteral(v)) { | |
| 855 | quasis[i] += v.quasis[0].value.cooked; | |
| 856 | quasis[i + 1] = | |
| 857 | v.quasis[v.quasis.length - 1].value.cooked + (quasis[i + 1] || ""); | |
| 858 | quasis.splice( | |
| 859 | i + 1, | |
| 860 | 0, | |
| 861 | ...v.quasis.slice(1, -1).map((q) => q.value.cooked) | |
| 862 | ); | |
| 863 | expressions.splice(i, 1, ...v.expressions); | |
| 864 | i += v.expressions.length; | |
| 865 | } else if (_compiler.types.isStringLiteral(v) || typeof v === "string") { | |
| 866 | const value = _compiler.types.isStringLiteral(v) ? v.value : v; | |
| 867 | quasis[i] += value + quasis[i + 1]; | |
| 868 | expressions.splice(i, 1); | |
| 869 | quasis.splice(i + 1, 1); | |
| 870 | } | |
| 871 | } | |
| 872 | ||
| 873 | if (!expressions.length) { | |
| 874 | // No expression, just return a literal or empty. | |
| 875 | const literal = quasis.join(""); | |
| 876 | return literal === "" ? undefined : _compiler.types.stringLiteral(literal); | |
| 877 | } | |
| 878 | ||
| 879 | if ( | |
| 880 | expressions.length === 1 && | |
| 881 | quasis.length === 2 && | |
| 882 | quasis.every(isEmptyString)) | |
| 883 | { | |
| 884 | // Only expression `${expr}` just return the expr. | |
| 885 | return expressions[0]; | |
| 886 | } | |
| 887 | ||
| 888 | // Do it. | |
| 889 | return _compiler.types.templateLiteral(quasis.map(getTemplateElement), expressions); | |
| 890 | } | |
| 891 | ||
| 892 | // parseArgs | |
| 893 | function parseArgs(file, str, sourceStart, sourceEnd) { | |
| 894 | const parsed = parseExpression(file, `_(${str})`, sourceStart, sourceEnd, 2); | |
| 895 | ||
| 896 | if (parsed.type === "CallExpression") { | |
| 897 | return parsed.arguments; | |
| 898 | } | |
| 899 | ||
| 900 | return [ensureParseError(file, parsed, sourceStart, sourceEnd)]; | |
| 901 | } | |
| 902 | ||
| 903 | // parseExpression | |
| 904 | function parseExpression( | |
| 905 | file, | |
| 906 | str, | |
| 907 | sourceStart, | |
| 908 | sourceEnd, | |
| 909 | sourceOffset) | |
| 910 | { | |
| 911 | return tryParse(file, true, str, sourceStart, sourceEnd, sourceOffset); | |
| 912 | } | |
| 913 | ||
| 914 | // parseParams | |
| 915 | function parseParams(file, str, sourceStart, sourceEnd) { | |
| 916 | const parsed = parseExpression( | |
| 917 | file, | |
| 918 | `(${str})=>{}`, | |
| 919 | sourceStart, | |
| 920 | sourceEnd, | |
| 921 | 1 | |
| 922 | ); | |
| 923 | ||
| 924 | if (parsed.type === "ArrowFunctionExpression") { | |
| 925 | return parsed.params; | |
| 926 | } | |
| 927 | ||
| 928 | return [ensureParseError(file, parsed, sourceStart, sourceEnd)]; | |
| 929 | } | |
| 930 | ||
| 931 | // parseStatements | |
| 932 | function parseStatements( | |
| 933 | file, | |
| 934 | str, | |
| 935 | sourceStart, | |
| 936 | sourceEnd, | |
| 937 | sourceOffset) | |
| 938 | { | |
| 939 | return tryParse(file, false, str, sourceStart, sourceEnd, sourceOffset); | |
| 940 | } | |
| 941 | ||
| 942 | // parseTemplateLiteral | |
| 943 | function parseTemplateLiteral(file, str, sourceStart, sourceEnd) { | |
| 944 | const parsed = parseExpression( | |
| 945 | file, | |
| 946 | "`" + str + "`", | |
| 947 | sourceStart, | |
| 948 | sourceEnd, | |
| 949 | 1 | |
| 950 | ); | |
| 951 | ||
| 952 | if (parsed.type === "TemplateLiteral") { | |
| 953 | return _compiler.types.templateLiteral(parsed.quasis, parsed.expressions); | |
| 954 | } | |
| 955 | ||
| 956 | return ensureParseError(file, parsed, sourceStart, sourceEnd); | |
| 957 | } | |
| 958 | ||
| 959 | // parseTypeArgs | |
| 960 | function parseTypeArgs(file, str, sourceStart, sourceEnd) { | |
| 961 | const parsed = parseExpression(file, `_<${str}>`, sourceStart, sourceEnd, 2); | |
| 962 | ||
| 963 | if (parsed.type === "TSInstantiationExpression") { | |
| 964 | // typeArguments is Flow only (not TS), we need to use typeParameters | |
| 965 | return parsed.typeParameters; | |
| 966 | } | |
| 967 | ||
| 968 | return [ensureParseError(file, parsed, sourceStart, sourceEnd)]; | |
| 969 | } | |
| 970 | ||
| 971 | // parseTypeParams | |
| 972 | function parseTypeParams(file, str, sourceStart, sourceEnd) { | |
| 973 | const parsed = parseExpression( | |
| 974 | file, | |
| 975 | `<${str}>()=>{}`, | |
| 976 | sourceStart, | |
| 977 | sourceEnd, | |
| 978 | 1 | |
| 979 | ); | |
| 980 | ||
| 981 | if (parsed.type === "ArrowFunctionExpression") { | |
| 982 | return parsed.typeParameters; | |
| 983 | } | |
| 984 | ||
| 985 | return [ensureParseError(file, parsed, sourceStart, sourceEnd)]; | |
| 986 | } | |
| 987 | ||
| 988 | // parseVar | |
| 989 | function parseVar(file, str, sourceStart, sourceEnd) { | |
| 990 | const parsed = parseExpression( | |
| 991 | file, | |
| 992 | `(${str})=>{}`, | |
| 993 | sourceStart, | |
| 994 | sourceEnd, | |
| 995 | 1 | |
| 996 | ); | |
| 997 | ||
| 998 | if (parsed.type === "ArrowFunctionExpression" && parsed.params.length === 1) { | |
| 999 | return parsed.params[0]; | |
| 1000 | } | |
| 1001 | ||
| 1002 | return ensureParseError(file, parsed, sourceStart, sourceEnd); | |
| 1003 | } | |
| 1004 | ||
| 1005 | // registerMacro | |
| 1006 | function registerMacro(path, name) { | |
| 1007 | const { file } = path.hub; | |
| 1008 | const markoMeta = file.metadata.marko; | |
| 1009 | const macroNames = markoMeta[MACRO_NAMES_KEY]; | |
| 1010 | ||
| 1011 | if (macroNames) { | |
| 1012 | if (macroNames[name]) { | |
| 1013 | (0, _diagnostics.diagnosticWarn)(path, { | |
| 1014 | label: `A macro with the name "${name}" already exists.`, | |
| 1015 | fix() { | |
| 1016 | findParentTag(path).remove(); | |
| 1017 | } | |
| 1018 | }); | |
| 1019 | } | |
| 1020 | macroNames[name] = true; | |
| 1021 | } else { | |
| 1022 | markoMeta[MACRO_NAMES_KEY] = { [name]: true }; | |
| 1023 | } | |
| 1024 | } | |
| 1025 | ||
| 1026 | // resolveRelativePath | |
| 1027 | function resolveRelativePath(file, request) { | |
| 1028 | if (request.startsWith(FS_START)) { | |
| 1029 | request = (0, _relativeImportPath.relativeImportPath)(file.opts.filename, request); | |
| 1030 | } | |
| 1031 | ||
| 1032 | if (file.markoOpts.optimize) { | |
| 1033 | request = request.replace( | |
| 1034 | /(^|\/node-modules\/)marko\/src\//, | |
| 1035 | "$1marko/dist/" | |
| 1036 | ); | |
| 1037 | } | |
| 1038 | ||
| 1039 | return request; | |
| 1040 | } | |
| 1041 | ||
| 1042 | // resolveTagImport | |
| 1043 | function resolveTagImport(path, request) { | |
| 1044 | const { | |
| 1045 | hub: { file } | |
| 1046 | } = path; | |
| 1047 | if (request[0] === "<") { | |
| 1048 | const tagName = request.slice(1, -1); | |
| 1049 | const tagDef = (0, _taglib.getTagDefForTagName)(file, tagName); | |
| 1050 | const tagEntry = tagDef && (tagDef.renderer || tagDef.template); | |
| 1051 | const relativePath = tagEntry && (0, _imports.resolveRelativePath)(file, tagEntry); | |
| 1052 | ||
| 1053 | if (!relativePath) { | |
| 1054 | throw path.buildCodeFrameError( | |
| 1055 | `Unable to find entry point for custom tag <${tagName}>.` | |
| 1056 | ); | |
| 1057 | } | |
| 1058 | ||
| 1059 | return relativePath; | |
| 1060 | } | |
| 1061 | ||
| 1062 | if (request.endsWith(".marko")) { | |
| 1063 | return (0, _imports.resolveRelativePath)(file, request); | |
| 1064 | } | |
| 1065 | } | |
| 1066 | ||
| 1067 | // withLoc | |
| 1068 | function withLoc(file, node, start, end) { | |
| 1069 | node.loc = getLocRange(file, start, end); | |
| 1070 | node.start = start; | |
| 1071 | node.end = end; | |
| 1072 | return node; | |
| 1073 | } |
src/file-viewer/views/canvas.client.ts created+30| ... | ... | @@ -0,0 +1,30 @@ |
| 1 | // __esModule | |
| 2 | true | |
| 3 | ||
| 4 | // getFile | |
| 5 | function getFile() { | |
| 6 | if (currentFile) { | |
| 7 | return currentFile; | |
| 8 | } | |
| 9 | ||
| 10 | throw new Error("Unable to access Marko File outside of a compilation"); | |
| 11 | } | |
| 12 | ||
| 13 | // getFileInternal | |
| 14 | function getFileInternal() { | |
| 15 | return currentFile; | |
| 16 | } | |
| 17 | ||
| 18 | // getProgram | |
| 19 | function getProgram() { | |
| 20 | if (currentFile) { | |
| 21 | return currentFile.path; | |
| 22 | } | |
| 23 | ||
| 24 | throw new Error("Unable to access Marko Program outside of a compilation"); | |
| 25 | } | |
| 26 | ||
| 27 | // setFileInternal | |
| 28 | function setFileInternal(file) { | |
| 29 | return currentFile = file; | |
| 30 | } |
src/file-viewer/views/canvas.marko created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | import { Script } from "#sitegen"; | |
| 2 | ||
| 3 | export interface Input { | |
| 4 | script: string; | |
| 5 | } | |
| 6 | ||
| 7 | <Script src="./canvas.client.ts" />; | |
| 8 | ||
| 9 | <canvas#canvas | |
| 10 | data-canvas=script | |
| 11 | data-standalone="true" | |
| 12 | style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;" | |
| 13 | /> |
src/file-viewer/views/file-viewer.client.ts created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | // __esModule | |
| 2 | true | |
| 3 | ||
| 4 | // DiagnosticType | |
| 5 | [object Object] | |
| 6 | ||
| 7 | // diagnosticDeprecate | |
| 8 | function diagnosticDeprecate(path, options) { | |
| 9 | add(DiagnosticType.Deprecation, path, options); | |
| 10 | } | |
| 11 | ||
| 12 | // diagnosticError | |
| 13 | function diagnosticError(path, options) { | |
| 14 | add(DiagnosticType.Error, path, options); | |
| 15 | } | |
| 16 | ||
| 17 | // diagnosticSuggest | |
| 18 | function diagnosticSuggest(path, options) { | |
| 19 | add(DiagnosticType.Suggestion, path, options); | |
| 20 | } | |
| 21 | ||
| 22 | // diagnosticWarn | |
| 23 | function diagnosticWarn(path, options) { | |
| 24 | add(DiagnosticType.Warning, path, options); | |
| 25 | } |
src/file-viewer/views/file-viewer.css created+33| ... | ... | @@ -0,0 +1,33 @@ |
| 1 | // default | |
| 2 | class PluginPass { | |
| 3 | constructor(file, key, options, isAsync) { | |
| 4 | this._map = new Map(); | |
| 5 | this.key = void 0; | |
| 6 | this.file = void 0; | |
| 7 | this.opts = void 0; | |
| 8 | this.cwd = void 0; | |
| 9 | this.filename = void 0; | |
| 10 | this.isAsync = void 0; | |
| 11 | this.key = key; | |
| 12 | this.file = file; | |
| 13 | this.opts = options || {}; | |
| 14 | this.cwd = file.opts.cwd; | |
| 15 | this.filename = file.opts.filename; | |
| 16 | this.isAsync = isAsync; | |
| 17 | } | |
| 18 | set(key, val) { | |
| 19 | this._map.set(key, val); | |
| 20 | } | |
| 21 | get(key) { | |
| 22 | return this._map.get(key); | |
| 23 | } | |
| 24 | availableHelper(name, versionRange) { | |
| 25 | return this.file.availableHelper(name, versionRange); | |
| 26 | } | |
| 27 | addHelper(name) { | |
| 28 | return this.file.addHelper(name); | |
| 29 | } | |
| 30 | buildCodeFrameError(node, msg, _Error) { | |
| 31 | return this.file.buildCodeFrameError(node, msg, _Error); | |
| 32 | } | |
| 33 | } |
src/file-viewer/views/file-viewer.tsx created+1007| ... | ... | @@ -0,0 +1,1007 @@ |
| 1 | import "./file-viewer.css"; | |
| 2 | import assert from "node:assert"; | |
| 3 | import path, { dirname } from "node:path"; | |
| 4 | import { MediaFile } from "../db.ts"; | |
| 5 | import { useInlineScript } from "../framework/page-resources.ts"; | |
| 6 | import { escapeUri, formatDuration, formatSize } from "../media/share.ts"; | |
| 7 | import { | |
| 8 | highlightConvo, | |
| 9 | highlightHashComments, | |
| 10 | highlightLinksInTextView, | |
| 11 | } from "../media/text-formatting.ts"; | |
| 12 | import { TestID } from "../test/id.ts"; | |
| 13 | import { ForEveryone } from "../media/cotyledon.tsx"; | |
| 14 | ||
| 15 | export const theme = { | |
| 16 | bg: "#312652", | |
| 17 | fg: "#f0f0ff", | |
| 18 | primary: "#fabe32", | |
| 19 | }; | |
| 20 | ||
| 21 | export const extensionToViewer: { | |
| 22 | [key: string]: (props: { | |
| 23 | file: MediaFile; | |
| 24 | siblingFiles?: MediaFile[]; | |
| 25 | }) => any; | |
| 26 | } = { | |
| 27 | ".html": IframeView, | |
| 28 | ".txt": TextView, | |
| 29 | ".md": TextView, | |
| 30 | ".mp4": VideoView, | |
| 31 | ".mkv": VideoView, | |
| 32 | ".webm": VideoView, | |
| 33 | ".avi": VideoView, | |
| 34 | ".mov": VideoView, | |
| 35 | ".mp3": AudioView, | |
| 36 | ".flac": AudioView, | |
| 37 | ".wav": AudioView, | |
| 38 | ".ogg": AudioView, | |
| 39 | ".m4a": AudioView, | |
| 40 | ".jpg": ImageView, | |
| 41 | ".jpeg": ImageView, | |
| 42 | ".png": ImageView, | |
| 43 | ".gif": ImageView, | |
| 44 | ".webp": ImageView, | |
| 45 | ".avif": ImageView, | |
| 46 | ".heic": ImageView, | |
| 47 | ".svg": ImageView, | |
| 48 | ".chat": ChatView, | |
| 49 | ".json": CodeView, | |
| 50 | ".jsonc": CodeView, | |
| 51 | ".toml": CodeView, | |
| 52 | ".ts": CodeView, | |
| 53 | ".js": CodeView, | |
| 54 | ".tsx": CodeView, | |
| 55 | ".jsx": CodeView, | |
| 56 | ".css": CodeView, | |
| 57 | ".py": CodeView, | |
| 58 | ".lua": CodeView, | |
| 59 | ".sh": CodeView, | |
| 60 | ".bat": CodeView, | |
| 61 | ".ps1": CodeView, | |
| 62 | ".cmd": CodeView, | |
| 63 | ".yaml": CodeView, | |
| 64 | ".yml": CodeView, | |
| 65 | ".xml": CodeView, | |
| 66 | ".zig": CodeView, | |
| 67 | ".astro": CodeView, | |
| 68 | ".mdx": CodeView, | |
| 69 | ".php": CodeView, | |
| 70 | ".diff": CodeView, | |
| 71 | ".patch": CodeView, | |
| 72 | }; | |
| 73 | ||
| 74 | export default function MediaList({ | |
| 75 | file, | |
| 76 | hasCotyledonCookie, | |
| 77 | }: { | |
| 78 | file: MediaFile; | |
| 79 | hasCotyledonCookie: boolean; | |
| 80 | }) { | |
| 81 | useInlineScript("file_viewer"); | |
| 82 | ||
| 83 | const dirs: MediaFile[] = []; | |
| 84 | let dir: MediaFile | null = file; | |
| 85 | assert(dir); | |
| 86 | do { | |
| 87 | dirs.unshift(dir); | |
| 88 | dir = dir?.getParent(); | |
| 89 | } while (dir); | |
| 90 | const parts = file.path.split("/"); | |
| 91 | ||
| 92 | const isCotyledon = parseInt(parts[1]) < 2025; | |
| 93 | ||
| 94 | return ( | |
| 95 | <div class={"files" + (isCotyledon ? " ctld ctld-" + parts[1] : "")}> | |
| 96 | {dirs.map((entry, i) => { | |
| 97 | const isLast = i === dirs.length - 1; | |
| 98 | return ( | |
| 99 | <MediaPanel | |
| 100 | file={entry} | |
| 101 | isLast={isLast} | |
| 102 | activeFilename={parts[i + 1]} | |
| 103 | hasCotyledonCookie={hasCotyledonCookie} | |
| 104 | /> | |
| 105 | ); | |
| 106 | })} | |
| 107 | </div> | |
| 108 | ); | |
| 109 | } | |
| 110 | ||
| 111 | const specialCaseViewers: Record<string, any> = { | |
| 112 | "/2024/for everyone": ForEveryone, | |
| 113 | }; | |
| 114 | ||
| 115 | export function MediaPanel({ | |
| 116 | file, | |
| 117 | isLast, | |
| 118 | activeFilename, | |
| 119 | hasCotyledonCookie, | |
| 120 | }: { | |
| 121 | file: MediaFile; | |
| 122 | isLast: boolean; | |
| 123 | activeFilename: string | null; | |
| 124 | hasCotyledonCookie: boolean; | |
| 125 | }) { | |
| 126 | const showsActions = file.path !== "/2024/for everyone"; | |
| 127 | const label = file.path === "/" | |
| 128 | ? ( | |
| 129 | "clo's files" | |
| 130 | ) | |
| 131 | : ( | |
| 132 | <> | |
| 133 | {file.kind === MediaFile.Kind.directory | |
| 134 | ? ( | |
| 135 | <span> | |
| 136 | {file.basename} | |
| 137 | <span class="bold-slash">/</span> | |
| 138 | </span> | |
| 139 | ) | |
| 140 | : ( | |
| 141 | <> | |
| 142 | {file.basename} | |
| 143 | <div style="flex:1" /> | |
| 144 | {showsActions && ( | |
| 145 | <div class="actions"> | |
| 146 | <a | |
| 147 | class="custom full-screen" | |
| 148 | href={`/file${escapeUri(file.path)}?view=embed`} | |
| 149 | aria-label="full screen" | |
| 150 | > | |
| 151 | </a> | |
| 152 | <a | |
| 153 | class="custom download" | |
| 154 | href={`/file${escapeUri(file.path)}?dl`} | |
| 155 | aria-label="download" | |
| 156 | > | |
| 157 | </a> | |
| 158 | </div> | |
| 159 | )} | |
| 160 | </> | |
| 161 | )} | |
| 162 | </> | |
| 163 | ); | |
| 164 | let View = specialCaseViewers[file.path] ?? | |
| 165 | (extensionToViewer[file.extension.toLowerCase()] as any) ?? | |
| 166 | DownloadView; | |
| 167 | if (View === VideoView && file.size > 500_000_000) { | |
| 168 | View = DownloadViewTooBig; | |
| 169 | } | |
| 170 | const mobileBtn = file.path !== "/" && ( | |
| 171 | <a | |
| 172 | href={`/file${escapeUri(path.dirname(file.path).replace(/\/$/, ""))}`} | |
| 173 | className="custom mobile-back" | |
| 174 | /> | |
| 175 | ); | |
| 176 | const canvases: Record<string, string> = { | |
| 177 | "/2017": "2017", | |
| 178 | "/2018": "2018", | |
| 179 | "/2019": "2019", | |
| 180 | "/2020": "2020", | |
| 181 | "/2021": "2021", | |
| 182 | "/2022": "2022", | |
| 183 | "/2023": "2023", | |
| 184 | "/2024": "2024", | |
| 185 | }; | |
| 186 | if (canvases[file.path]) { | |
| 187 | useInlineScript(`canvas_${canvases[file.path]}` as any); | |
| 188 | } | |
| 189 | return ( | |
| 190 | <div | |
| 191 | class={(isLast ? "panel last" : "panel") + | |
| 192 | (file.path === "/" ? " first" : "")} | |
| 193 | > | |
| 194 | {canvases[file.path] && ( | |
| 195 | <canvas | |
| 196 | class="fullscreen-canvas" | |
| 197 | data-canvas={canvases[file.path]} | |
| 198 | > | |
| 199 | </canvas> | |
| 200 | )} | |
| 201 | {file.kind === MediaFile.Kind.directory && ( | |
| 202 | <a class="custom header" href={`/file${escapeUri(file.path)}`}> | |
| 203 | <div class="ico ico-dir-open"></div> | |
| 204 | {label} | |
| 205 | </a> | |
| 206 | )} | |
| 207 | <div class="header"> | |
| 208 | {mobileBtn} | |
| 209 | {<div class={`ico ico-${fileIcon(file, true)}`}></div>} | |
| 210 | {label} | |
| 211 | </div> | |
| 212 | ||
| 213 | {file.kind === MediaFile.Kind.directory | |
| 214 | ? ( | |
| 215 | <DirView | |
| 216 | dir={file} | |
| 217 | activeFilename={activeFilename} | |
| 218 | isLast={isLast} | |
| 219 | hasCotyledonCookie={hasCotyledonCookie} | |
| 220 | /> | |
| 221 | ) | |
| 222 | : ( | |
| 223 | <div | |
| 224 | class={`content file-view${ | |
| 225 | View.class ? " file-view-" + View.class : "" | |
| 226 | }`} | |
| 227 | > | |
| 228 | <View file={file} /> | |
| 229 | </div> | |
| 230 | )} | |
| 231 | </div> | |
| 232 | ); | |
| 233 | } | |
| 234 | ||
| 235 | const readmeFile = "readme.txt"; | |
| 236 | const priorityFiles = ["readme.txt", "index.html"]; | |
| 237 | ||
| 238 | function sorterFromDirSort(dirsort: string) { | |
| 239 | const array: string[] = JSON.parse(dirsort); | |
| 240 | if (array.length === 1 && array[0] === "a-z") { | |
| 241 | return sortAlphabetically; | |
| 242 | } | |
| 243 | return (a: MediaFile, b: MediaFile) => { | |
| 244 | const aIndex = array.indexOf(a.basename); | |
| 245 | const bIndex = array.indexOf(b.basename); | |
| 246 | if (bIndex == -1 && aIndex >= 0) return -1; | |
| 247 | if (aIndex == -1 && bIndex >= 0) return 1; | |
| 248 | if (aIndex >= 0 && bIndex >= 0) { | |
| 249 | return aIndex - bIndex; | |
| 250 | } | |
| 251 | sortDefault(a, b); | |
| 252 | }; | |
| 253 | } | |
| 254 | ||
| 255 | function sortNumerically(a: MediaFile, b: MediaFile) { | |
| 256 | const n1 = parseInt(a.basenameWithoutExt); | |
| 257 | const n2 = parseInt(a.basenameWithoutExt); | |
| 258 | return n1 - n2; | |
| 259 | } | |
| 260 | ||
| 261 | function sortDefault(a: MediaFile, b: MediaFile) { | |
| 262 | // First check if either file is in the priority list | |
| 263 | const aIndex = priorityFiles.indexOf(a.basename.toLowerCase()); | |
| 264 | const bIndex = priorityFiles.indexOf(b.basename.toLowerCase()); | |
| 265 | ||
| 266 | if (aIndex !== -1 && bIndex !== -1) { | |
| 267 | // Both are priority files, sort by priority order | |
| 268 | return aIndex - bIndex; | |
| 269 | } else if (aIndex !== -1) { | |
| 270 | // Only a is a priority file | |
| 271 | return -1; | |
| 272 | } else if (bIndex !== -1) { | |
| 273 | // Only b is a priority file | |
| 274 | return 1; | |
| 275 | } | |
| 276 | ||
| 277 | // Then sort directories before files | |
| 278 | if (a.kind !== b.kind) { | |
| 279 | return a.kind === MediaFile.Kind.directory ? -1 : 1; | |
| 280 | } | |
| 281 | ||
| 282 | // Finally sort by date (newest first), then by name (a-z) if dates are the same | |
| 283 | const dateComparison = b.date.getTime() - a.date.getTime(); | |
| 284 | if (dateComparison !== 0) { | |
| 285 | return dateComparison; | |
| 286 | } | |
| 287 | // If dates are the same, sort alphabetically by name | |
| 288 | return a.basename.localeCompare(b.basename); | |
| 289 | } | |
| 290 | ||
| 291 | function sortAlphabetically(a: MediaFile, b: MediaFile) { | |
| 292 | // First check if either file is in the priority list | |
| 293 | const aIndex = priorityFiles.indexOf(a.basename.toLowerCase()); | |
| 294 | const bIndex = priorityFiles.indexOf(b.basename.toLowerCase()); | |
| 295 | ||
| 296 | if (aIndex !== -1 && bIndex !== -1) { | |
| 297 | // Both are priority files, sort by priority order | |
| 298 | return aIndex - bIndex; | |
| 299 | } else if (aIndex !== -1) { | |
| 300 | // Only a is a priority file | |
| 301 | return -1; | |
| 302 | } else if (bIndex !== -1) { | |
| 303 | // Only b is a priority file | |
| 304 | return 1; | |
| 305 | } | |
| 306 | ||
| 307 | // If dates are the same, sort alphabetically by name | |
| 308 | return a.basename.localeCompare(b.basename); | |
| 309 | } | |
| 310 | ||
| 311 | function isNumericallyOrdered(files: MediaFile[]) { | |
| 312 | return !files.some((x) => Number.isNaN(parseInt(x.basenameWithoutExt))); | |
| 313 | } | |
| 314 | ||
| 315 | function sortChronologicalStartToEnd(a: MediaFile, b: MediaFile) { | |
| 316 | const aIndex = priorityFiles.indexOf(a.basename.toLowerCase()); | |
| 317 | const bIndex = priorityFiles.indexOf(b.basename.toLowerCase()); | |
| 318 | ||
| 319 | if (aIndex !== -1 && bIndex !== -1) { | |
| 320 | return aIndex - bIndex; | |
| 321 | } else if (aIndex !== -1) { | |
| 322 | return -1; | |
| 323 | } else if (bIndex !== -1) { | |
| 324 | return 1; | |
| 325 | } | |
| 326 | ||
| 327 | const dateComparison = a.date.getTime() - b.date.getTime(); | |
| 328 | if (dateComparison !== 0) { | |
| 329 | return dateComparison; | |
| 330 | } | |
| 331 | return a.basename.localeCompare(b.basename); | |
| 332 | } | |
| 333 | ||
| 334 | const sortOverrides: Record<string, (a: MediaFile, b: MediaFile) => number> = { | |
| 335 | "/2024": sortChronologicalStartToEnd, | |
| 336 | "/2023": sortChronologicalStartToEnd, | |
| 337 | "/2022": sortChronologicalStartToEnd, | |
| 338 | "/2021": sortChronologicalStartToEnd, | |
| 339 | "/2020": sortChronologicalStartToEnd, | |
| 340 | "/2019": sortChronologicalStartToEnd, | |
| 341 | "/2018": sortChronologicalStartToEnd, | |
| 342 | "/2017": sortChronologicalStartToEnd, | |
| 343 | }; | |
| 344 | ||
| 345 | function DirView({ | |
| 346 | dir, | |
| 347 | activeFilename, | |
| 348 | isLast, | |
| 349 | hasCotyledonCookie, | |
| 350 | }: { | |
| 351 | dir: MediaFile; | |
| 352 | activeFilename: string | null; | |
| 353 | isLast: boolean; | |
| 354 | hasCotyledonCookie: boolean; | |
| 355 | }) { | |
| 356 | if (dir.path === "/") { | |
| 357 | return ( | |
| 358 | <RootDirView | |
| 359 | dir={dir} | |
| 360 | activeFilename={activeFilename} | |
| 361 | isLast={isLast} | |
| 362 | hasCotyledonCookie={hasCotyledonCookie} | |
| 363 | /> | |
| 364 | ); | |
| 365 | } | |
| 366 | const isCotyledon = parseInt(dir.path.split("/")?.[1]) < 2025; | |
| 367 | const unsortedFiles = dir | |
| 368 | .getChildren() | |
| 369 | .filter((f) => !f.basenameWithoutExt.startsWith("_unlisted")); | |
| 370 | const sorter = dir.dirsort | |
| 371 | ? sorterFromDirSort(dir.dirsort) | |
| 372 | : isNumericallyOrdered(unsortedFiles) | |
| 373 | ? sortNumerically | |
| 374 | : isCotyledon | |
| 375 | ? sortChronologicalStartToEnd | |
| 376 | : (sortOverrides[dir.path] ?? sortDefault); | |
| 377 | const sortedFiles = unsortedFiles.sort(sorter); | |
| 378 | const readme = sortedFiles.find((f) => f.basename === readmeFile); | |
| 379 | if (readme && isLast) activeFilename ||= readmeFile; | |
| 380 | const main = ( | |
| 381 | <div class="content primary"> | |
| 382 | <ul> | |
| 383 | {sortedFiles.map((file) => { | |
| 384 | return ( | |
| 385 | <ListItem file={file} active={activeFilename === file.basename} /> | |
| 386 | ); | |
| 387 | })} | |
| 388 | </ul> | |
| 389 | </div> | |
| 390 | ); | |
| 391 | ||
| 392 | if (readme && isLast) { | |
| 393 | return ( | |
| 394 | <div class="hsplit"> | |
| 395 | <ReadmeView file={readme} siblingFiles={sortedFiles} /> | |
| 396 | {main} | |
| 397 | </div> | |
| 398 | ); | |
| 399 | } | |
| 400 | ||
| 401 | return main; | |
| 402 | } | |
| 403 | ||
| 404 | const unknownDate = new Date("1970-01-03"); | |
| 405 | const unknownDateWithKnownYear = new Date("1970-02-20"); | |
| 406 | ||
| 407 | function ListItem({ | |
| 408 | file, | |
| 409 | active, | |
| 410 | noDate, | |
| 411 | }: { | |
| 412 | file: MediaFile; | |
| 413 | active: boolean; | |
| 414 | noDate?: boolean; | |
| 415 | }) { | |
| 416 | const dateTime = file.date; | |
| 417 | let shortDate = dateTime < unknownDateWithKnownYear | |
| 418 | ? ( | |
| 419 | dateTime < unknownDate | |
| 420 | ? ( | |
| 421 | "??.??.??" | |
| 422 | ) | |
| 423 | : <>xx.xx.{21 + Math.floor(dateTime.getTime() / 86400000)}</> | |
| 424 | ) | |
| 425 | : ( | |
| 426 | `${(dateTime.getMonth() + 1).toString().padStart(2, "0")}.${ | |
| 427 | dateTime | |
| 428 | .getDate() | |
| 429 | .toString() | |
| 430 | .padStart(2, "0") | |
| 431 | }.${dateTime.getFullYear().toString().slice(2)}` | |
| 432 | ); | |
| 433 | ||
| 434 | let basenameWithoutExt = file.basenameWithoutExt; | |
| 435 | ||
| 436 | let meta = file.kind === MediaFile.Kind.directory | |
| 437 | ? formatSize(file.size) | |
| 438 | : (file.duration ?? 0) > 0 | |
| 439 | ? formatDuration(file.duration!) | |
| 440 | : null; | |
| 441 | if (meta && dirname(file.path) !== "/") { | |
| 442 | meta = `(${meta})`; | |
| 443 | } | |
| 444 | ||
| 445 | const isReadme = file.basename === readmeFile; | |
| 446 | ||
| 447 | return ( | |
| 448 | <li> | |
| 449 | <a | |
| 450 | class={(active ? "active " : "") + (isReadme ? "readme " : "") + | |
| 451 | "li custom"} | |
| 452 | href={file.extension === ".lnk" | |
| 453 | ? file.contents | |
| 454 | : isReadme | |
| 455 | ? `/file${escapeUri(path.dirname(file.path).replace(/\/$/, ""))}` | |
| 456 | : `/file${escapeUri(file.path)}`} | |
| 457 | > | |
| 458 | <span> | |
| 459 | {file.basename === readmeFile | |
| 460 | ? <>{!noDate && <div class="line"></div>}</> | |
| 461 | : <>{!noDate && <date>{shortDate}</date>}</>} | |
| 462 | <span class="size mobile">{meta}</span> | |
| 463 | </span> | |
| 464 | ||
| 465 | <span> | |
| 466 | {path.dirname(file.path) !== "/" && ( | |
| 467 | <span className={`ico ico-${fileIcon(file)}`}></span> | |
| 468 | )} | |
| 469 | {basenameWithoutExt} | |
| 470 | <span class="ext">{file.extension}</span> | |
| 471 | {file.kind === MediaFile.Kind.directory | |
| 472 | ? <span class="bold-slash">/</span> | |
| 473 | : ( | |
| 474 | "" | |
| 475 | )} | |
| 476 | ||
| 477 | <span class="size desktop">{meta}</span> | |
| 478 | </span> | |
| 479 | </a> | |
| 480 | </li> | |
| 481 | ); | |
| 482 | } | |
| 483 | ||
| 484 | function fileIcon(file: MediaFile, dirOpen?: boolean) { | |
| 485 | if (file.kind === MediaFile.Kind.directory) { | |
| 486 | return dirOpen ? "dir-open" : "dir"; | |
| 487 | } | |
| 488 | if (file.basename === "readme.txt") { | |
| 489 | return "readme"; | |
| 490 | } | |
| 491 | if (file.path === "/2024/for everyone") { | |
| 492 | return "snow"; | |
| 493 | } | |
| 494 | const ext = path.extname(file.basename).toLowerCase(); | |
| 495 | if ( | |
| 496 | ext === ".mp4" || | |
| 497 | ext === ".mkv" || | |
| 498 | ext === ".webm" || | |
| 499 | ext === ".avi" || | |
| 500 | ext === ".mov" | |
| 501 | ) { | |
| 502 | return "video"; | |
| 503 | } | |
| 504 | if ( | |
| 505 | ext === ".mp3" || | |
| 506 | ext === ".flac" || | |
| 507 | ext === ".wav" || | |
| 508 | ext === ".ogg" || | |
| 509 | ext === ".m4a" | |
| 510 | ) { | |
| 511 | return "audio"; | |
| 512 | } | |
| 513 | if ( | |
| 514 | ext === ".jpg" || | |
| 515 | ext === ".jpeg" || | |
| 516 | ext === ".png" || | |
| 517 | ext === ".gif" || | |
| 518 | ext === ".webp" || | |
| 519 | ext === ".avif" || | |
| 520 | ext === ".heic" || | |
| 521 | ext === ".svg" | |
| 522 | ) { | |
| 523 | return "image"; | |
| 524 | } | |
| 525 | if (ext === ".comp" || ext === ".fuse" || ext === ".setting") return "fusion"; | |
| 526 | if (ext === ".txt" || ext === ".md") return "text"; | |
| 527 | if (ext === ".html") return "webpage"; | |
| 528 | if (ext === ".blend") return "blend"; | |
| 529 | if ( | |
| 530 | ext === ".zip" || | |
| 531 | ext === ".rar" || | |
| 532 | ext === ".7z" || | |
| 533 | ext === ".tar" || | |
| 534 | ext === ".gz" || | |
| 535 | ext === ".bz2" || | |
| 536 | ext === ".xz" | |
| 537 | ) { | |
| 538 | return "archive"; | |
| 539 | } | |
| 540 | if (ext === ".lnk") return "link"; | |
| 541 | if (ext === ".chat") return "chat"; | |
| 542 | if ( | |
| 543 | ext === ".ts" || | |
| 544 | ext === ".js" || | |
| 545 | ext === ".tsx" || | |
| 546 | ext === ".jsx" || | |
| 547 | ext === ".css" || | |
| 548 | ext === ".py" || | |
| 549 | ext === ".lua" || | |
| 550 | ext === ".sh" || | |
| 551 | ext === ".bat" || | |
| 552 | ext === ".ps1" || | |
| 553 | ext === ".cmd" || | |
| 554 | ext === ".php" | |
| 555 | ) { | |
| 556 | return "code"; | |
| 557 | } | |
| 558 | if (ext === ".json" || ext === ".toml" || ext === ".yaml" || ext === ".yml") { | |
| 559 | return "json"; | |
| 560 | } | |
| 561 | return "file"; | |
| 562 | } | |
| 563 | ||
| 564 | function RootDirView({ | |
| 565 | dir, | |
| 566 | activeFilename, | |
| 567 | isLast, | |
| 568 | hasCotyledonCookie, | |
| 569 | }: { | |
| 570 | dir: MediaFile; | |
| 571 | activeFilename: string | null; | |
| 572 | isLast: boolean; | |
| 573 | hasCotyledonCookie: boolean; | |
| 574 | }) { | |
| 575 | const children = dir.getChildren(); | |
| 576 | let readme: MediaFile | null = null; | |
| 577 | ||
| 578 | const groups = { | |
| 579 | // years 2025 and onwards | |
| 580 | years: [] as MediaFile[], | |
| 581 | // named categories | |
| 582 | categories: [] as MediaFile[], | |
| 583 | // years 2017 to 2024 | |
| 584 | cotyledon: [] as MediaFile[], | |
| 585 | }; | |
| 586 | const colorMap = { | |
| 587 | years: "#a2ff91", | |
| 588 | categories: "#9c91ff", | |
| 589 | cotyledon: "#ff91ca", | |
| 590 | }; | |
| 591 | for (const child of children) { | |
| 592 | const basename = child.basename; | |
| 593 | if (basename === readmeFile) { | |
| 594 | readme = child; | |
| 595 | continue; | |
| 596 | } | |
| 597 | ||
| 598 | const year = basename.match(/^(\d{4})/); | |
| 599 | if (year) { | |
| 600 | const n = parseInt(year[1]); | |
| 601 | if (n >= 2025) { | |
| 602 | groups.years.push(child); | |
| 603 | } else { | |
| 604 | groups.cotyledon.push(child); | |
| 605 | } | |
| 606 | } else { | |
| 607 | groups.categories.push(child); | |
| 608 | } | |
| 609 | } | |
| 610 | ||
| 611 | if (readme && isLast) activeFilename ||= readmeFile; | |
| 612 | ||
| 613 | const main = ( | |
| 614 | <div class="content primary"> | |
| 615 | {readme && ( | |
| 616 | <ul> | |
| 617 | <ListItem | |
| 618 | file={readme} | |
| 619 | active={activeFilename === readmeFile} | |
| 620 | noDate | |
| 621 | /> | |
| 622 | </ul> | |
| 623 | )} | |
| 624 | {Object.entries(groups).map(([key, files]) => { | |
| 625 | if (key === "cotyledon" && !hasCotyledonCookie) { | |
| 626 | return null; | |
| 627 | } | |
| 628 | if (key === "years" || key === "cotyledon") { | |
| 629 | files.sort((a, b) => { | |
| 630 | return b.basename.localeCompare(a.basename); | |
| 631 | }); | |
| 632 | } else { | |
| 633 | files.sort((a, b) => { | |
| 634 | return a.basename.localeCompare(b.basename); | |
| 635 | }); | |
| 636 | } | |
| 637 | return ( | |
| 638 | <div class="group" data-group={key}> | |
| 639 | <h3 style={`color:${colorMap[key as keyof typeof colorMap]};`}> | |
| 640 | {key} | |
| 641 | </h3> | |
| 642 | <ul> | |
| 643 | {files.map((file) => ( | |
| 644 | <ListItem | |
| 645 | file={file} | |
| 646 | active={activeFilename === file.basename} | |
| 647 | noDate={key === "cotyledon"} | |
| 648 | /> | |
| 649 | ))} | |
| 650 | </ul> | |
| 651 | </div> | |
| 652 | ); | |
| 653 | })} | |
| 654 | </div> | |
| 655 | ); | |
| 656 | ||
| 657 | if (readme && isLast) { | |
| 658 | return ( | |
| 659 | <div class="hsplit"> | |
| 660 | <ReadmeView | |
| 661 | file={readme} | |
| 662 | siblingFiles={children} | |
| 663 | extra={!hasCotyledonCookie && ( | |
| 664 | <> | |
| 665 | <a href="/file/cotyledon" class="custom cotyledon-link desktop"> | |
| 666 | cotyledon | |
| 667 | </a> | |
| 668 | </> | |
| 669 | )} | |
| 670 | /> | |
| 671 | {main} | |
| 672 | {!hasCotyledonCookie && ( | |
| 673 | <> | |
| 674 | <a href="/file/cotyledon" class="custom cotyledon-link mobile"> | |
| 675 | cotyledon | |
| 676 | </a> | |
| 677 | </> | |
| 678 | )} | |
| 679 | </div> | |
| 680 | ); | |
| 681 | } | |
| 682 | ||
| 683 | return main; | |
| 684 | } | |
| 685 | ||
| 686 | function TextView({ | |
| 687 | file, | |
| 688 | siblingFiles = [], | |
| 689 | }: { | |
| 690 | file: MediaFile; | |
| 691 | siblingFiles?: MediaFile[]; | |
| 692 | }) { | |
| 693 | const contents = file.contents; | |
| 694 | ||
| 695 | if (file.path.startsWith("/2021/phoenix-write/maps")) { | |
| 696 | const basename = file.basename; | |
| 697 | if (basename.includes("map") && basename.endsWith(".txt")) { | |
| 698 | return ( | |
| 699 | <pre | |
| 700 | style="white-space:normal;max-width:73ch" | |
| 701 | dangerouslySetInnerHTML={{ | |
| 702 | __html: highlightHashComments(contents), | |
| 703 | }} | |
| 704 | ></pre> | |
| 705 | ); | |
| 706 | } | |
| 707 | } | |
| 708 | ||
| 709 | return ( | |
| 710 | <pre | |
| 711 | style="white-space:pre-wrap;max-width:70ch" | |
| 712 | dangerouslySetInnerHTML={{ | |
| 713 | __html: highlightLinksInTextView( | |
| 714 | contents, | |
| 715 | siblingFiles.filter((f) => f.kind === MediaFile.Kind.file), | |
| 716 | ), | |
| 717 | }} | |
| 718 | ></pre> | |
| 719 | ); | |
| 720 | } | |
| 721 | ||
| 722 | function ChatView({ file }: { file: MediaFile }) { | |
| 723 | const contents = file.contents; | |
| 724 | return ( | |
| 725 | <div | |
| 726 | style="max-width:70ch" | |
| 727 | class="convo" | |
| 728 | dangerouslySetInnerHTML={{ | |
| 729 | __html: highlightConvo(contents), | |
| 730 | }} | |
| 731 | > | |
| 732 | </div> | |
| 733 | ); | |
| 734 | } | |
| 735 | ||
| 736 | function CodeView({ file }: { file: MediaFile }) { | |
| 737 | const highlightedContents = file.contents; | |
| 738 | if (!highlightedContents) { | |
| 739 | if (file.size > 1_000_000) { | |
| 740 | return <DownloadViewTooBig file={file} />; | |
| 741 | } | |
| 742 | return <DownloadViewCodeNotComputed file={file} />; | |
| 743 | } | |
| 744 | return ( | |
| 745 | <pre | |
| 746 | style="white-space:pre-wrap" | |
| 747 | dangerouslySetInnerHTML={{ | |
| 748 | __html: highlightedContents, | |
| 749 | }} | |
| 750 | /> | |
| 751 | ); | |
| 752 | } | |
| 753 | ||
| 754 | function VideoView({ file }: { file: MediaFile }) { | |
| 755 | useInlineScript("video_player"); | |
| 756 | const dimensions = file.parseDimensions() ?? { width: 1920, height: 1080 }; | |
| 757 | return ( | |
| 758 | <> | |
| 759 | <video | |
| 760 | controls | |
| 761 | preload | |
| 762 | style={`background:transparent;aspect-ratio:${ | |
| 763 | simplifyFraction( | |
| 764 | dimensions.width, | |
| 765 | dimensions.height, | |
| 766 | ) | |
| 767 | }`} | |
| 768 | > | |
| 769 | <source | |
| 770 | src={"/file" + file.path} | |
| 771 | type={contentTypeFromExt(file.path)} | |
| 772 | /> | |
| 773 | </video> | |
| 774 | </> | |
| 775 | ); | |
| 776 | } | |
| 777 | ||
| 778 | function AudioView({ | |
| 779 | file, | |
| 780 | onlyAudio = false, | |
| 781 | }: { | |
| 782 | file: MediaFile; | |
| 783 | onlyAudio?: boolean; | |
| 784 | }) { | |
| 785 | const parent = file.getParent(); | |
| 786 | let lyricsFile: MediaFile | null = null; | |
| 787 | if (parent && !onlyAudio) { | |
| 788 | const siblings = parent.getChildren(); | |
| 789 | // show lyrics only if | |
| 790 | // - this media file is the only audio file | |
| 791 | // - there is lyrics.txt in the same directory | |
| 792 | const audioFiles = siblings.filter( | |
| 793 | (f) => | |
| 794 | f.kind === MediaFile.Kind.file && | |
| 795 | extensionToViewer[path.extname(f.basename)] === AudioView, | |
| 796 | ); | |
| 797 | if ( | |
| 798 | audioFiles.length === 1 && | |
| 799 | siblings.find((f) => f.basename === "lyrics.txt") | |
| 800 | ) { | |
| 801 | lyricsFile = siblings.find((f) => f.basename === "lyrics.txt")!; | |
| 802 | } | |
| 803 | } | |
| 804 | return ( | |
| 805 | <> | |
| 806 | <audio controls preload="none" style="width:100%;"> | |
| 807 | <source | |
| 808 | src={"/file" + file.path} | |
| 809 | type={`audio/${path.extname(file.path).substring(1)}`} | |
| 810 | /> | |
| 811 | </audio> | |
| 812 | {lyricsFile && ( | |
| 813 | <div class="lyrics"> | |
| 814 | <h2 style="margin-left:1rem;margin-bottom:-0.2rem;margin-top:0.5rem;"> | |
| 815 | lyrics | |
| 816 | </h2> | |
| 817 | <TextView file={lyricsFile} siblingFiles={[]} /> | |
| 818 | </div> | |
| 819 | )} | |
| 820 | </> | |
| 821 | ); | |
| 822 | } | |
| 823 | ||
| 824 | function ImageView({ file }: { file: MediaFile }) { | |
| 825 | return ( | |
| 826 | <img | |
| 827 | src={"/file" + file.path} | |
| 828 | alt={file.basename} | |
| 829 | style="max-width:100%;max-height:100%;object-fit:contain;margin:auto;display:block;" | |
| 830 | /> | |
| 831 | ); | |
| 832 | } | |
| 833 | ||
| 834 | function IframeView({ file }: { file: MediaFile }) { | |
| 835 | return ( | |
| 836 | <> | |
| 837 | <iframe | |
| 838 | style="flex:1;height:100%;border:none" | |
| 839 | src={"/file" + file.path + "?view=embed"} | |
| 840 | /> | |
| 841 | </> | |
| 842 | ); | |
| 843 | } | |
| 844 | ||
| 845 | const helptexts: Record<string, any> = { | |
| 846 | ".blend": ( | |
| 847 | <p> | |
| 848 | <code>.blend</code> files can be open in{" "} | |
| 849 | <a href="https://www.blender.org" target="_blank"> | |
| 850 | Blender | |
| 851 | </a> | |
| 852 | . | |
| 853 | </p> | |
| 854 | ), | |
| 855 | ".comp": <FusionHelp ext="comp" name="composition files (project/scene)" />, | |
| 856 | ".setting": <FusionHelp ext="setting" name="macro/template" />, | |
| 857 | ".fuse": <FusionHelp ext="setting" name="FUSE Plugin (lua source code)" />, | |
| 858 | }; | |
| 859 | ||
| 860 | function FusionHelp({ ext, name }: { ext: string; name: string }) { | |
| 861 | return ( | |
| 862 | <> | |
| 863 | <p> | |
| 864 | <code>.{ext}</code> files are{" "} | |
| 865 | <a | |
| 866 | href="https://www.blackmagicdesign.com/products/fusion" | |
| 867 | target="_blank" | |
| 868 | > | |
| 869 | Fusion | |
| 870 | </a>{" "} | |
| 871 | {name}. | |
| 872 | </p> | |
| 873 | <p> | |
| 874 | Blackmagic Fusion is paid-only since mid-2019 (the "Studio" version). | |
| 875 | The last free version is Fusion 9.0.2, which exists for MacOS, Linux, | |
| 876 | and Microsoft Windows on{" "} | |
| 877 | <a href="https://www.blackmagicdesign.com/support" target="_blank"> | |
| 878 | Blackmagic Design's support page | |
| 879 | </a> | |
| 880 | . | |
| 881 | </p> | |
| 882 | <p> | |
| 883 | Alternatively, you can use DaVinci Resolve, however I do not recommend | |
| 884 | it as it is very unstable, at least as of when I last tried version 18. | |
| 885 | </p> | |
| 886 | </> | |
| 887 | ); | |
| 888 | } | |
| 889 | function DownloadView({ file }: { file: MediaFile }) { | |
| 890 | const help = helptexts[path.extname(file.basename)]; | |
| 891 | return help | |
| 892 | ? ( | |
| 893 | <> | |
| 894 | {help} | |
| 895 | <p> | |
| 896 | File download:{" "} | |
| 897 | <a href={"/file" + file.path + "?dl"} aria-label="download"> | |
| 898 | {file.basename} | |
| 899 | </a> | |
| 900 | . | |
| 901 | </p> | |
| 902 | </> | |
| 903 | ) | |
| 904 | : ( | |
| 905 | <> | |
| 906 | <p> | |
| 907 | No built-in viewer for <code>{path.extname(file.basename)}</code>{" "} | |
| 908 | files. | |
| 909 | </p> | |
| 910 | <p> | |
| 911 | Instead, you may download it:{" "} | |
| 912 | <a href={"/file" + file.path + "?dl"} aria-label="download"> | |
| 913 | {file.basename} | |
| 914 | </a> | |
| 915 | . | |
| 916 | </p> | |
| 917 | </> | |
| 918 | ); | |
| 919 | } | |
| 920 | ||
| 921 | function DownloadViewTooBig({ file }: { file: MediaFile }) { | |
| 922 | return ( | |
| 923 | <div style="padding:0.5rem"> | |
| 924 | <p> | |
| 925 | This file is too large to view in the browser right now ( | |
| 926 | <code>{formatSize(file.size)}</code>). Later, this file viewer will | |
| 927 | properly process video files for web streaming. | |
| 928 | </p> | |
| 929 | <p> | |
| 930 | Instead, you may download it:{" "} | |
| 931 | <a href={"/file" + file.path + "?dl"} aria-label="download"> | |
| 932 | {file.basename} | |
| 933 | </a> | |
| 934 | . | |
| 935 | </p> | |
| 936 | </div> | |
| 937 | ); | |
| 938 | } | |
| 939 | ||
| 940 | function DownloadViewCodeNotComputed({ file }: { file: MediaFile }) { | |
| 941 | return ( | |
| 942 | <div style="padding:0.5rem"> | |
| 943 | <p> | |
| 944 | This file has not been pre-processed for code highlighting. This either | |
| 945 | means it is not a valid UTF-8 text file (<code>.ts</code>{" "} | |
| 946 | can be TypeScript or an MPEG "Transport Stream"), or there is a bug in | |
| 947 | the file indexing logic. | |
| 948 | </p> | |
| 949 | <p> | |
| 950 | Instead, you may download it:{" "} | |
| 951 | <a href={"/file" + file.path + "?dl"} aria-label="download"> | |
| 952 | {file.basename} | |
| 953 | </a> | |
| 954 | . | |
| 955 | </p> | |
| 956 | </div> | |
| 957 | ); | |
| 958 | } | |
| 959 | ||
| 960 | function ReadmeView({ | |
| 961 | file, | |
| 962 | siblingFiles, | |
| 963 | extra, | |
| 964 | }: { | |
| 965 | file: MediaFile; | |
| 966 | siblingFiles: MediaFile[]; | |
| 967 | extra?: any; | |
| 968 | }) { | |
| 969 | // let showFileCandidates = siblingFiles.filter(f => | |
| 970 | // f.kind === MediaFile.Kind.file | |
| 971 | // && (extensionToViewer[path.extname(f.basename)] === AudioView | |
| 972 | // || extensionToViewer[path.extname(f.basename)] === VideoView) | |
| 973 | // ); | |
| 974 | return ( | |
| 975 | <div class="content readme" data-testid={TestID.Readme}> | |
| 976 | {/* {showFileCandidates.length === 1 && <AudioView file={showFileCandidates[0]} onlyAudio />} */} | |
| 977 | <TextView file={file} siblingFiles={siblingFiles} /> | |
| 978 | {extra} | |
| 979 | </div> | |
| 980 | ); | |
| 981 | } | |
| 982 | ||
| 983 | const gcd = (a: number, b: number): number => (b ? gcd(b, a % b) : a); | |
| 984 | function simplifyFraction(n: number, d: number) { | |
| 985 | const divisor = gcd(n, d); | |
| 986 | return `${n / divisor}/${d / divisor}`; | |
| 987 | } | |
| 988 | ||
| 989 | export function contentTypeFromExt(src: string) { | |
| 990 | src = src.toLowerCase(); | |
| 991 | if (src.endsWith(".m3u8")) return "application/x-mpegURL"; | |
| 992 | if (src.endsWith(".webm")) return "video/webm"; | |
| 993 | if (src.endsWith(".mp4")) return "video/mp4"; | |
| 994 | if (src.endsWith(".mkv")) return "video/x-matroska"; | |
| 995 | if (src.endsWith(".avi")) return "video/x-msvideo"; | |
| 996 | if (src.endsWith(".mov")) return "video/quicktime"; | |
| 997 | if (src.endsWith(".ogg")) return "video/ogg"; | |
| 998 | throw new Error("Unknown video extension: " + path.extname(src)); | |
| 999 | } | |
| 1000 | ||
| 1001 | // Add class properties to the components | |
| 1002 | AudioView.class = "audio"; | |
| 1003 | ImageView.class = "image"; | |
| 1004 | VideoView.class = "video"; | |
| 1005 | TextView.class = "text"; | |
| 1006 | DownloadView.class = "download"; | |
| 1007 | CodeView.class = "code"; |
src/global.css created+120| ... | ... | @@ -0,0 +1,120 @@ |
| 1 | @font-face { | |
| 2 | font-family: "recu"; | |
| 3 | src: url("/recultramin.woff2") format("woff2"); | |
| 4 | font-weight: 400 750; | |
| 5 | font-style: normal; | |
| 6 | font-display: swap; | |
| 7 | font-variation-settings: "CASL" 0.25, "MONO" 0; | |
| 8 | font-style: oblique -15deg 0deg; | |
| 9 | unicode-range: U+0020-007E; | |
| 10 | } | |
| 11 | @font-face { | |
| 12 | font-family: "rmo"; | |
| 13 | src: url("/recmono.woff2") format("woff2"); | |
| 14 | font-weight: 400 800; | |
| 15 | font-style: normal; | |
| 16 | font-display: swap; | |
| 17 | font-variation-settings: "CASL" 0.25, "MONO" 1; | |
| 18 | font-style: oblique -15deg 0deg; | |
| 19 | unicode-range: U+0020-007E; | |
| 20 | } | |
| 21 | @font-face { | |
| 22 | font-family: "rmo"; | |
| 23 | src: url("/recmonorest.woff2") format("woff2"); | |
| 24 | font-weight: 400 800; | |
| 25 | font-style: normal; | |
| 26 | font-display: swap; | |
| 27 | font-variation-settings: "CASL" 0.25, "MONO" 1; | |
| 28 | font-style: oblique -15deg 0deg; | |
| 29 | unicode-range: | |
| 30 | U+00C0-00FF, | |
| 31 | U+00A9, | |
| 32 | U+2190-2193, | |
| 33 | U+2018, | |
| 34 | U+2019, | |
| 35 | U+201C, | |
| 36 | U+201D, | |
| 37 | U+2022, | |
| 38 | U+00A0-00A8, | |
| 39 | U+00AA-00BF, | |
| 40 | U+2194-2199, | |
| 41 | U+0100-017F; | |
| 42 | } | |
| 43 | ||
| 44 | *, | |
| 45 | ::before, | |
| 46 | ::after { | |
| 47 | box-sizing: border-box; | |
| 48 | margin: 0; | |
| 49 | padding: 0; | |
| 50 | } | |
| 51 | ||
| 52 | body { | |
| 53 | font-family: "recu", sans-serif; | |
| 54 | background-color: var(--bg); | |
| 55 | color: var(--fg); | |
| 56 | word-break: break-word; | |
| 57 | } | |
| 58 | ||
| 59 | main { | |
| 60 | margin: auto; | |
| 61 | max-width: 80ch; | |
| 62 | padding: 1em; | |
| 63 | } | |
| 64 | ||
| 65 | h1 { | |
| 66 | font-size: 2em; | |
| 67 | } | |
| 68 | ||
| 69 | h1, | |
| 70 | h2, | |
| 71 | h3, | |
| 72 | h4, | |
| 73 | h5, | |
| 74 | h6, | |
| 75 | p, | |
| 76 | figure, | |
| 77 | ul, | |
| 78 | ol { | |
| 79 | margin-top: 1em; | |
| 80 | margin-bottom: 1rem; | |
| 81 | } | |
| 82 | ||
| 83 | ul, | |
| 84 | ol { | |
| 85 | padding-left: 1em; | |
| 86 | } | |
| 87 | ||
| 88 | li { | |
| 89 | margin: 0.5em 0; | |
| 90 | } | |
| 91 | ||
| 92 | a { | |
| 93 | text-decoration: none; | |
| 94 | } | |
| 95 | a:where([href]:not(.custom)) { | |
| 96 | color: var(--primary); | |
| 97 | margin: -0.2rem -0.3rem; | |
| 98 | padding: 0.2rem 0.3rem; | |
| 99 | border-radius: 4px; | |
| 100 | &:hover { | |
| 101 | text-decoration: underline; | |
| 102 | font-weight: 600; | |
| 103 | color: var(--primary); | |
| 104 | color: lch(from var(--primary) calc(l) calc(c + 30) h); | |
| 105 | } | |
| 106 | &:active { | |
| 107 | text-decoration: underline; | |
| 108 | font-weight: 600; | |
| 109 | text-decoration: none; | |
| 110 | color: black; | |
| 111 | background-color: var(--primary); | |
| 112 | background-color: lch(from var(--primary) l calc(c + 20) calc(h - 90)); | |
| 113 | text-shadow: none; | |
| 114 | } | |
| 115 | } | |
| 116 | ||
| 117 | code { | |
| 118 | font-family: "rmo", monospace; | |
| 119 | font-size: inherit; | |
| 120 | } |
src/pages/admin/index.mdx created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | export default ({children}) => <main>{children}</main> | |
| 2 | export const meta = { title: 'paper clover admin' }; | |
| 3 | ||
| 4 | # hi chloe | |
| 5 | ||
| 6 | remember that you're loved. | |
| 7 | ||
| 8 | - [question admin view](/admin/q+a) | |
| 9 | - [question inbox](/admin/q+a/inbox) |
src/pages/admin/login.fail.marko created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | export const meta = { title: 'paper clover admin' }; | |
| 2 | ||
| 3 | <main> | |
| 4 | <h1>login</h1> | |
| 5 | <p style="color: red">incorrect credentials</p> | |
| 6 | <form method="POST"> | |
| 7 | <button style="padding: 0.5rem">submit</button> | |
| 8 | </form> | |
| 9 | </main> |
src/pages/admin/login.marko created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | export const meta = { title: 'paper clover admin' }; | |
| 2 | ||
| 3 | <main> | |
| 4 | <h1>login</h1> | |
| 5 | <form method="POST"> | |
| 6 | <button style="padding: 0.5rem">submit</button> | |
| 7 | </form> | |
| 8 | </main> |
src/pages/index.css created+26| ... | ... | @@ -0,0 +1,26 @@ |
| 1 | main { | |
| 2 | padding-top: 1em; | |
| 3 | display: flex; | |
| 4 | flex-direction: column; | |
| 5 | justify-content: center; | |
| 6 | align-items: center; | |
| 7 | height: 100vh; | |
| 8 | & > div { | |
| 9 | width: 450px; | |
| 10 | margin: auto; | |
| 11 | } | |
| 12 | } | |
| 13 | h1 { | |
| 14 | margin: -1.5rem 0 3rem 0; | |
| 15 | font-size: 5rem; | |
| 16 | font-weight: 400; | |
| 17 | } | |
| 18 | h2 { | |
| 19 | font-size: 1.15rem; | |
| 20 | text-decoration: underline; | |
| 21 | color: #000e; | |
| 22 | } | |
| 23 | hr { | |
| 24 | width: 100%; | |
| 25 | border: 1px solid #000b; | |
| 26 | } |
src/pages/index.marko created+46| ... | ... | @@ -0,0 +1,46 @@ |
| 1 | import type { Metadata } from "next-metadata"; | |
| 2 | import "./index.css"; | |
| 3 | ||
| 4 | export const theme = { | |
| 5 | bg: "#fff", | |
| 6 | fg: "#666", | |
| 7 | }; | |
| 8 | static const title = "paper clover"; | |
| 9 | static const description = "and then we knew, just like paper airplanes: that we could fly..."; | |
| 10 | export const meta: Metadata = { | |
| 11 | title, | |
| 12 | description, | |
| 13 | openGraph: { | |
| 14 | title, | |
| 15 | description, | |
| 16 | type: "website", | |
| 17 | url: "https://paperclover.net", | |
| 18 | }, | |
| 19 | generator: "clover", | |
| 20 | alternates: { | |
| 21 | canonical: "https://paperclover.net", | |
| 22 | types: { | |
| 23 | "application/rss+xml": [ | |
| 24 | { url: "rss.xml", title: "rss" }, | |
| 25 | ], | |
| 26 | }, | |
| 27 | }, | |
| 28 | }; | |
| 29 | ||
| 30 | <main> | |
| 31 | <div> | |
| 32 | <h2>posts</h2> | |
| 33 | <p>song: <span>in the summer</span> (coming soon, 2025-07-12)</p> | |
| 34 | <p>song: <a href="/waterfalls">waterfalls</a> (2025-01-01)</p> | |
| 35 | <h2>things</h2> | |
| 36 | <p><a href="/q+a">questions and answers</a></p> | |
| 37 | <p><a href="/file">file browser</a></p> | |
| 38 | <p><a href="/subscribe">mailing list</a></p> | |
| 39 | <p><a href="/donate">donate</a></p> | |
| 40 | <p><a href="/rss.xml">rss feed</a></p> | |
| 41 | <p style="position: relative; z-index: 10"><a href="/discord">discord</a></p> | |
| 42 | <h1> | |
| 43 | paper clover | |
| 44 | </h1> | |
| 45 | </div> | |
| 46 | </main> |
src/pages/license.mdx created+31| ... | ... | @@ -0,0 +1,31 @@ |
| 1 | export const CurrentYear = () => new Date().getFullYear(); | |
| 2 | export default ({children}) => <main>{children}</main>; | |
| 3 | export const theme = { | |
| 4 | fg: '#ffe', | |
| 5 | bg: '#434', | |
| 6 | h1: '#f5f', | |
| 7 | }; | |
| 8 | export const meta = { | |
| 9 | title: "paper clover's media license" | |
| 10 | }; | |
| 11 | ||
| 12 | # paper clover's media license | |
| 13 | ||
| 14 | copyright 2018-<CurrentYear/> paper clover | |
| 15 | ||
| 16 | all rights reserved | |
| 17 | ||
| 18 | <br/> | |
| 19 | ||
| 20 | this license applies to all "media files" on paperclover.net, which specifically | |
| 21 | refers to downloadable files available within the [paperclover.net/file](/file) directory. | |
| 22 | ||
| 23 | permission is given to download, view, or otherwise store a copy of any media | |
| 24 | file. however, media files must not be uploaded, published, or otherwise | |
| 25 | distributed over the internet or other electronical communication devices. | |
| 26 | all uses of media files must be used for non-commercial purposes. | |
| 27 | ||
| 28 | paperclover.net provides media files as-is, without any warranty or liability. | |
| 29 | ||
| 30 | the above rules are in place to preserve my sanity.<br/> | |
| 31 | thank you for respecting them. |
src/pages/subscribe.client.ts created| Binary files /dev/null and b/src/pages/subscribe.client.ts differ |
src/pages/subscribe.css created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | // default | |
| 2 | class ConfigError extends Error { | |
| 3 | constructor(message, filename) { | |
| 4 | super(message); | |
| 5 | (0, _rewriteStackTrace.expectedError)(this); | |
| 6 | if (filename) (0, _rewriteStackTrace.injectVirtualStackFrame)(this, filename); | |
| 7 | } | |
| 8 | } |
src/pages/subscribe.marko created| Binary files /dev/null and b/src/pages/subscribe.marko differ |
src/q+a/artifacts.ts created| Binary files /dev/null and b/src/q+a/artifacts.ts differ |
src/q+a/backend.ts created+242| ... | ... | @@ -0,0 +1,242 @@ |
| 1 | import { type Context, Hono } from "hono"; | |
| 2 | import type { ContentfulStatusCode } from "hono/utils/http-status"; | |
| 3 | import { | |
| 4 | adjectives, | |
| 5 | animals, | |
| 6 | colors, | |
| 7 | uniqueNamesGenerator, | |
| 8 | } from "unique-names-generator"; | |
| 9 | import { hasAdminToken } from "../admin"; | |
| 10 | import { regenerateAssets, serveAsset } from "../assets"; | |
| 11 | import { PendingQuestion, Question } from "../db"; | |
| 12 | import { renderDynamicPage } from "../framework/dynamic-pages"; | |
| 13 | import { getQuestionImage } from "./question_image"; | |
| 14 | import { formatQuestionId, questionIdToTimestamp } from "./QuestionRender"; | |
| 15 | import process from "node:process"; | |
| 16 | ||
| 17 | const PROXYCHECK_API_KEY = process.env.PROXYCHECK_API_KEY; | |
| 18 | ||
| 19 | let isStale = false; | |
| 20 | ||
| 21 | const app = new Hono(); | |
| 22 | // Main page | |
| 23 | app.get("/q+a", async (c) => { | |
| 24 | if (isStale) { | |
| 25 | await regenerateAssets(); | |
| 26 | isStale = false; | |
| 27 | } | |
| 28 | if (hasAdminToken(c)) { | |
| 29 | return serveAsset(c, "/admin/q+a", 200); | |
| 30 | } | |
| 31 | return serveAsset(c, "/q+a", 200); | |
| 32 | }); | |
| 33 | // Submit form | |
| 34 | app.post("/q+a", async (c) => { | |
| 35 | const form = await c.req.formData(); | |
| 36 | let text = form.get("text"); | |
| 37 | if (typeof text !== "string") { | |
| 38 | return questionFailure(c, 400, "Bad Request"); | |
| 39 | } | |
| 40 | text = text.trim(); | |
| 41 | const input = { | |
| 42 | date: new Date(), | |
| 43 | prompt: text, | |
| 44 | sourceName: "unknown", | |
| 45 | sourceLocation: "unknown", | |
| 46 | sourceVPN: null, | |
| 47 | }; | |
| 48 | ||
| 49 | input.date.setMilliseconds(0); | |
| 50 | ||
| 51 | if (text.length <= 0) { | |
| 52 | return questionFailure(c, 400, "Content is too short", text); | |
| 53 | } | |
| 54 | ||
| 55 | if (text.length > 16000) { | |
| 56 | return questionFailure(c, 400, "Content is too long", text); | |
| 57 | } | |
| 58 | ||
| 59 | // Ban patterns | |
| 60 | if ( | |
| 61 | text.includes("hams" + "terko" + "mbat" + ".expert") // 2025-02-18. 3 occurrences. All VPN | |
| 62 | ) { | |
| 63 | // To prevent known automatic spam-bots from noticing something automatic is | |
| 64 | // happening, pretend that the question was successfully submitted. | |
| 65 | return sendSuccess(c, new Date()); | |
| 66 | } | |
| 67 | ||
| 68 | const ipAddr = c.req.header("cf-connecting-ip"); | |
| 69 | if (ipAddr) { | |
| 70 | input.sourceName = uniqueNamesGenerator({ | |
| 71 | dictionaries: [adjectives, colors, animals], | |
| 72 | separator: "-", | |
| 73 | seed: ipAddr + PROXYCHECK_API_KEY, | |
| 74 | }); | |
| 75 | } | |
| 76 | ||
| 77 | const cfIPCountry = c.req.header("cf-ipcountry"); | |
| 78 | if (cfIPCountry) { | |
| 79 | input.sourceLocation = cfIPCountry; | |
| 80 | } | |
| 81 | ||
| 82 | if (ipAddr && PROXYCHECK_API_KEY) { | |
| 83 | const proxyCheck = await fetch( | |
| 84 | `https://proxycheck.io/v2/?key=${PROXYCHECK_API_KEY}&risk=1&vpn=1`, | |
| 85 | { | |
| 86 | method: "POST", | |
| 87 | body: "ips=" + ipAddr, | |
| 88 | headers: { | |
| 89 | "Content-Type": "application/x-www-form-urlencoded", | |
| 90 | }, | |
| 91 | }, | |
| 92 | ).then((res) => res.json()); | |
| 93 | ||
| 94 | if (ipAddr && proxyCheck[ipAddr]) { | |
| 95 | if (proxyCheck[ipAddr].proxy === "yes") { | |
| 96 | input.sourceVPN = proxyCheck[ipAddr].operator?.name ?? | |
| 97 | proxyCheck[ipAddr].organisation ?? | |
| 98 | proxyCheck[ipAddr].provider ?? "unknown"; | |
| 99 | } | |
| 100 | if (Number(proxyCheck[ipAddr].risk) > 72) { | |
| 101 | return questionFailure( | |
| 102 | c, | |
| 103 | 403, | |
| 104 | "This IP address has been flagged as a high risk IP address. If you are using a VPN/Proxy, please disable it and try again.", | |
| 105 | text, | |
| 106 | ); | |
| 107 | } | |
| 108 | } | |
| 109 | } | |
| 110 | ||
| 111 | const date = Question.create( | |
| 112 | Question.Type.pending, | |
| 113 | JSON.stringify(input), | |
| 114 | input.date, | |
| 115 | ); | |
| 116 | await sendSuccess(c, date); | |
| 117 | }); | |
| 118 | async function sendSuccess(c: Context, date: Date) { | |
| 119 | if (c.req.header("Accept")?.includes("application/json")) { | |
| 120 | return c.json({ | |
| 121 | success: true, | |
| 122 | message: "ok", | |
| 123 | date: date.getTime(), | |
| 124 | id: formatQuestionId(date), | |
| 125 | }, { status: 200 }); | |
| 126 | } | |
| 127 | c.res = await renderDynamicPage(c.req.raw, "qa_success", { | |
| 128 | permalink: `https://paperclover.net/q+a/${formatQuestionId(date)}`, | |
| 129 | }); | |
| 130 | } | |
| 131 | // Question Permalink | |
| 132 | app.get("/q+a/:id", async (c, next) => { | |
| 133 | // from deadname era, the seconds used to be in the url. | |
| 134 | // this was removed so that the url can be crafted by hand. | |
| 135 | let id = c.req.param("id"); | |
| 136 | if (id.length === 12 && /^\d+$/.test(id)) { | |
| 137 | return c.redirect(`/q+a/${id.slice(0, 10)}`); | |
| 138 | } | |
| 139 | let image = false; | |
| 140 | if (id.endsWith(".png")) { | |
| 141 | image = true; | |
| 142 | id = id.slice(0, -4); | |
| 143 | } | |
| 144 | ||
| 145 | const timestamp = questionIdToTimestamp(id); | |
| 146 | if (!timestamp) return next(); | |
| 147 | const question = await Question.getByDate(timestamp); | |
| 148 | if (!question) return next(); | |
| 149 | ||
| 150 | if (image) { | |
| 151 | return getQuestionImage(question, c.req.method === "HEAD"); | |
| 152 | } | |
| 153 | return renderDynamicPage(c.req.raw, "qa_permalink", { question }); | |
| 154 | }); | |
| 155 | ||
| 156 | // Admin | |
| 157 | app.get("/admin/q+a", async (c) => { | |
| 158 | if (isStale) { | |
| 159 | await regenerateAssets(); | |
| 160 | isStale = false; | |
| 161 | } | |
| 162 | return serveAsset(c, "/admin/q+a", 200); | |
| 163 | }); | |
| 164 | app.get("/admin/q+a/inbox", async (c) => { | |
| 165 | return renderDynamicPage(c.req.raw, "qa_backend_inbox", {}); | |
| 166 | }); | |
| 167 | app.delete("/admin/q+a/:id", async (c, next) => { | |
| 168 | const id = c.req.param("id"); | |
| 169 | const timestamp = questionIdToTimestamp(id); | |
| 170 | if (!timestamp) return next(); | |
| 171 | const question = Question.getByDate(timestamp); | |
| 172 | if (!question) return next(); | |
| 173 | const deleteFull = c.req.header("X-Delete-Full") === "true"; | |
| 174 | if (deleteFull) { | |
| 175 | Question.deleteByQmid(question.qmid); | |
| 176 | } else { | |
| 177 | Question.rejectByQmid(question.qmid); | |
| 178 | } | |
| 179 | return c.json({ success: true, message: "ok" }); | |
| 180 | }); | |
| 181 | app.patch("/admin/q+a/:id", async (c, next) => { | |
| 182 | const id = c.req.param("id"); | |
| 183 | const timestamp = questionIdToTimestamp(id); | |
| 184 | if (!timestamp) return next(); | |
| 185 | const question = Question.getByDate(timestamp); | |
| 186 | if (!question) return next(); | |
| 187 | const form = await c.req.raw.json(); | |
| 188 | if (typeof form.text !== "string" || typeof form.type !== "number") { | |
| 189 | console.log("bad request"); | |
| 190 | return questionFailure(c, 400, "Bad Request"); | |
| 191 | } | |
| 192 | Question.updateByQmid(question.qmid, form.text, form.type); | |
| 193 | isStale = true; | |
| 194 | return c.json({ success: true, message: "ok" }); | |
| 195 | }); | |
| 196 | app.get("/admin/q+a/:id", async (c, next) => { | |
| 197 | const id = c.req.param("id"); | |
| 198 | const timestamp = questionIdToTimestamp(id); | |
| 199 | if (!timestamp) return next(); | |
| 200 | const question = Question.getByDate(timestamp); | |
| 201 | if (!question) return next(); | |
| 202 | ||
| 203 | let pendingInfo: null | PendingQuestion.JSON = null; | |
| 204 | if (question.type === Question.Type.pending) { | |
| 205 | pendingInfo = JSON.parse(question.text) as PendingQuestion.JSON; | |
| 206 | question.text = pendingInfo.prompt.trim().split("\n").map((line) => | |
| 207 | line.trim().length === 0 ? "" : `q: ${line.trim()}` | |
| 208 | ).join("\n") + "\n\n"; | |
| 209 | question.type = Question.Type.normal; | |
| 210 | } | |
| 211 | ||
| 212 | return renderDynamicPage(c.req.raw, "qa_backend_edit", { | |
| 213 | pendingInfo, | |
| 214 | question, | |
| 215 | }); | |
| 216 | }); | |
| 217 | ||
| 218 | app.get("/q+a/things/random", async (c) => { | |
| 219 | c.res = await renderDynamicPage(c.req.raw, "qa_things_random", {}); | |
| 220 | }); | |
| 221 | ||
| 222 | // 404 | |
| 223 | app.get("/q+a/*", async (c) => { | |
| 224 | return serveAsset(c, "/q+a/404", 404); | |
| 225 | }); | |
| 226 | ||
| 227 | async function questionFailure( | |
| 228 | c: Context, | |
| 229 | status: ContentfulStatusCode, | |
| 230 | message: string, | |
| 231 | content?: string, | |
| 232 | ) { | |
| 233 | if (c.req.header("Accept")?.includes("application/json")) { | |
| 234 | return c.json({ success: false, message, id: null }, { status }); | |
| 235 | } | |
| 236 | c.res = await renderDynamicPage(c.req.raw, "qa_fail", { | |
| 237 | error: message, | |
| 238 | content, | |
| 239 | }); | |
| 240 | } | |
| 241 | ||
| 242 | export { app as questionAnswerApp }; |
src/q+a/clover-markdown.tsx created+250| ... | ... | @@ -0,0 +1,250 @@ |
| 1 | function assert(condition: any, message?: string): asserts condition { | |
| 2 | if (!condition) { | |
| 3 | throw new Error(message || "Assertion failed"); | |
| 4 | } | |
| 5 | } | |
| 6 | ||
| 7 | export const customRules = defaultRules.clone(); | |
| 8 | ||
| 9 | /** A question mention, formatted like `#112233445566` */ | |
| 10 | customRules.insertBefore("em", { | |
| 11 | name: "mentionQuestion", | |
| 12 | match: inlineRegex(/^#([0-9]{10})(?:[0-9]{2})?/), | |
| 13 | parse(capture) { | |
| 14 | return { | |
| 15 | id: capture[1], | |
| 16 | }; | |
| 17 | }, | |
| 18 | }); | |
| 19 | ||
| 20 | /** A question mention, formatted like `@pheonix-write` */ | |
| 21 | customRules.insertBefore("em", { | |
| 22 | name: "mentionArtifact", | |
| 23 | match: inlineRegex(/^@([a-z0-9-]+)/i), | |
| 24 | parse(capture) { | |
| 25 | return { | |
| 26 | id: capture[1], | |
| 27 | }; | |
| 28 | }, | |
| 29 | }); | |
| 30 | ||
| 31 | /** The censor bar used for past deadnaming: #name# and #username# */ | |
| 32 | customRules.insertBefore("em", { | |
| 33 | name: "deadname", | |
| 34 | match: inlineRegex(/^#(name|username)#/i), | |
| 35 | parse(capture) { | |
| 36 | return { | |
| 37 | kind: capture[1], | |
| 38 | }; | |
| 39 | }, | |
| 40 | }); | |
| 41 | ||
| 42 | /** html block. allows arbitrary html, but rn only as an answer. formatted like `@html content here` */ | |
| 43 | customRules.insertBefore("paragraph", { | |
| 44 | name: "html", | |
| 45 | match: blockRegex(/^@html ((?:[^\n]|\n(?! *\n))+)(?:\n *)+\n/), | |
| 46 | parse(capture) { | |
| 47 | return { | |
| 48 | data: capture[1], | |
| 49 | }; | |
| 50 | }, | |
| 51 | }); | |
| 52 | ||
| 53 | /** for server-rendered search, mark can highlight the seen word */ | |
| 54 | customRules.insertBefore("paragraph", { | |
| 55 | name: "mark", | |
| 56 | match: inlineRegex(/^<!!mark!!>(.*?)<!!mark!!>/), | |
| 57 | parse: parseCaptureInline, | |
| 58 | }); | |
| 59 | ||
| 60 | /** Adjust autolinking to use my own components if possible */ | |
| 61 | customRules.insertBefore("url", { | |
| 62 | name: "autolink_mention", | |
| 63 | match: inlineRegex( | |
| 64 | /^https?:\/\/(?:paper(?:dave|clover)\.net|localhost:\d+)\/([^\s<]+[^<.,:;"')\]\s])/, | |
| 65 | ), | |
| 66 | parse(capture) { | |
| 67 | const qaMatch = capture[1].match(/^q\+a\/([0-9]{10}(?:[0-9]{2})?)/); | |
| 68 | if (qaMatch) { | |
| 69 | return { | |
| 70 | type: "mentionQuestion", | |
| 71 | id: qaMatch[1], | |
| 72 | }; | |
| 73 | } | |
| 74 | ||
| 75 | return { | |
| 76 | type: "link", | |
| 77 | content: [ | |
| 78 | { | |
| 79 | type: "text", | |
| 80 | content: capture[1], | |
| 81 | }, | |
| 82 | ], | |
| 83 | target: capture[1], | |
| 84 | title: undefined, | |
| 85 | }; | |
| 86 | }, | |
| 87 | }); | |
| 88 | ||
| 89 | export const questionRules = customRules.clone(); | |
| 90 | ||
| 91 | questionRules.remove("heading"); | |
| 92 | ||
| 93 | /** Input paragraphs. any paragraph prefixed with `i:` */ | |
| 94 | questionRules.insertBefore("paragraph", { | |
| 95 | name: "question", | |
| 96 | match: blockRegex(/^(?:q: [^\n]+(?:\n|$))+\s*/), | |
| 97 | parse(capture, parse, state) { | |
| 98 | const lines = capture[0] | |
| 99 | .trim() | |
| 100 | .split("\n") | |
| 101 | .map((line) => line.trim()) | |
| 102 | .filter((x) => x.length > 0) | |
| 103 | .map((x) => { | |
| 104 | assert(x.startsWith("q: ")); | |
| 105 | return x.slice(3); | |
| 106 | }); | |
| 107 | const content = lines.map((line, i) => { | |
| 108 | const parsed = parseInline(parse, line, state); | |
| 109 | if (i < lines.length - 1) { | |
| 110 | parsed.push({ type: "br" }); | |
| 111 | } | |
| 112 | return parsed; | |
| 113 | }).flat(); | |
| 114 | return { | |
| 115 | content, | |
| 116 | }; | |
| 117 | }, | |
| 118 | }); | |
| 119 | ||
| 120 | const cloverParser = createParser(questionRules); | |
| 121 | ||
| 122 | const basicNodeTagNames: Record<string, string> = { | |
| 123 | mark: "mark", | |
| 124 | question: "q-", | |
| 125 | paragraph: "p", | |
| 126 | hr: "hr", | |
| 127 | blockQuote: "blockquote", | |
| 128 | em: "em", | |
| 129 | strong: "strong", | |
| 130 | u: "u", | |
| 131 | del: "del", | |
| 132 | inlineCode: "code", | |
| 133 | br: "br", | |
| 134 | }; | |
| 135 | ||
| 136 | function BasicNode(node: ASTNode, children: any) { | |
| 137 | const E = basicNodeTagNames[node.type]; | |
| 138 | return <E>{children}</E>; | |
| 139 | } | |
| 140 | ||
| 141 | function ListRenderer(node: ASTNode, children: any[]) { | |
| 142 | const T = node.ordered ? "ol" : "ul"; | |
| 143 | return <T>{children.map((child) => <li>{child}</li>)}</T>; | |
| 144 | } | |
| 145 | ||
| 146 | function MarkdownLink(node: ASTNode, children: any[]) { | |
| 147 | return <a href={node.target}>{children}</a>; | |
| 148 | } | |
| 149 | ||
| 150 | function Deadname(node: ASTNode) { | |
| 151 | return <span class="dead">old {node.kind}</span>; | |
| 152 | } | |
| 153 | ||
| 154 | function QuestionRef(node: ASTNode) { | |
| 155 | if (node.id.length === 12) node.id = node.id.slice(0, 10); | |
| 156 | const timestamp = questionIdToTimestamp(node.id); | |
| 157 | if (!timestamp) { | |
| 158 | console.error(`Invalid question ref: ${node.id}`); | |
| 159 | return <span class="question-ref">[Invalid Question Ref: #{node.id}]</span>; | |
| 160 | } | |
| 161 | return ( | |
| 162 | <a class="custom question-ref" href={`/q+a/${node.id}`}> | |
| 163 | {formatQuestionTimestamp(timestamp)} | |
| 164 | </a> | |
| 165 | ); | |
| 166 | } | |
| 167 | ||
| 168 | function ArtifactRef(node: ASTNode) { | |
| 169 | const artifact = artifactMap[node.id]; | |
| 170 | if (!artifact) { | |
| 171 | return <span class="artifact">[Invalid Artifact Ref: @{node.id}]</span>; | |
| 172 | } | |
| 173 | const [title, url, type] = artifact; | |
| 174 | if (!url) { | |
| 175 | return <span>{title}</span>; | |
| 176 | } | |
| 177 | return <a class={`custom artifact artifact-${type}`} href={url}>{title}</a>; | |
| 178 | } | |
| 179 | ||
| 180 | const br = <br />; | |
| 181 | const ssrFunctions: any = { | |
| 182 | // Default Renders | |
| 183 | // heading: HeadingRenderer, | |
| 184 | hr: BasicNode, | |
| 185 | mark: BasicNode, | |
| 186 | blockQuote: BasicNode, | |
| 187 | // codeBlock: MarkdownCodeBlock, | |
| 188 | list: ListRenderer, | |
| 189 | // table: TableRenderer, | |
| 190 | paragraph: BasicNode, | |
| 191 | // tableSeparator: ???, | |
| 192 | link: MarkdownLink, | |
| 193 | // image: MarkdownImage, | |
| 194 | em: BasicNode, | |
| 195 | strong: BasicNode, | |
| 196 | u: BasicNode, | |
| 197 | del: BasicNode, | |
| 198 | inlineCode: BasicNode, | |
| 199 | br: () => br, | |
| 200 | newline: () => br, | |
| 201 | deadname: Deadname, | |
| 202 | ||
| 203 | // Custom Elements | |
| 204 | question: BasicNode, | |
| 205 | html: (node: any) => ssr.html(node.data), | |
| 206 | mentionQuestion: QuestionRef, | |
| 207 | mentionArtifact: ArtifactRef, | |
| 208 | }; | |
| 209 | ||
| 210 | function renderNode(node: string | ASTNode | ASTNode[]): any { | |
| 211 | if (typeof node === "string") { | |
| 212 | return node; | |
| 213 | } else if (Array.isArray(node)) { | |
| 214 | return node.flatMap((item) => renderNode(item)); | |
| 215 | } else if (node.type === "text") { | |
| 216 | return node.content; | |
| 217 | } else { | |
| 218 | const render = ssrFunctions[node.type]; | |
| 219 | if (!render) { | |
| 220 | throw new Error(`No renderer for node type "${node.type}"`); | |
| 221 | } | |
| 222 | const content = node.content as any; | |
| 223 | return render(node, content ? renderNode(content) : undefined); | |
| 224 | } | |
| 225 | } | |
| 226 | ||
| 227 | export namespace CloverMarkdown { | |
| 228 | export interface Props { | |
| 229 | text: string; | |
| 230 | inline?: boolean; | |
| 231 | } | |
| 232 | } | |
| 233 | ||
| 234 | export function CloverMarkdown(props: CloverMarkdown.Props) { | |
| 235 | const ast = cloverParser(props.text, { inline: !!props.inline }); | |
| 236 | return renderNode(ast); | |
| 237 | } | |
| 238 | ||
| 239 | import { artifactMap } from "./artifacts.ts"; | |
| 240 | import { formatQuestionTimestamp, questionIdToTimestamp } from "./format.ts"; | |
| 241 | import { | |
| 242 | type ASTNode, | |
| 243 | blockRegex, | |
| 244 | createParser, | |
| 245 | defaultRules, | |
| 246 | inlineRegex, | |
| 247 | parseCaptureInline, | |
| 248 | parseInline, | |
| 249 | } from "./simple-markdown.ts"; | |
| 250 | import * as ssr from "#ssr"; |
src/q+a/components/Question.marko created+37| ... | ... | @@ -0,0 +1,37 @@ |
| 1 | // Renders a `Question` entry including its markdown body. | |
| 2 | export interface Input { | |
| 3 | question: Question; | |
| 4 | admin?: boolean; | |
| 5 | } | |
| 6 | ||
| 7 | // 2024-12-31 05:00:00 EST | |
| 8 | static const transitionDate = 1735639200000; | |
| 9 | ||
| 10 | <const/{ question, admin } = input /> | |
| 11 | <const/{ id, date } = question/> | |
| 12 | ||
| 13 | <${"e-"} | |
| 14 | f=(date > transitionDate ? true : undefined) | |
| 15 | id=admin ? `q${id}` : undefined | |
| 16 | > | |
| 17 | <if=admin> | |
| 18 | <a | |
| 19 | style="margin-right: 0.5rem" | |
| 20 | href=`/admin/q+a/${id}` | |
| 21 | >[EDIT]</a> | |
| 22 | </> | |
| 23 | <a> | |
| 24 | <time | |
| 25 | datetime=formatQuestionISOTimestamp(date) | |
| 26 | >${formatQuestionTimestamp(date)}</time> | |
| 27 | </a> | |
| 28 | ||
| 29 | <CloverMarkdown text=question.text /> | |
| 30 | </> | |
| 31 | ||
| 32 | // this singleton script will make all the '<time>' tags clickable. | |
| 33 | client import "./clickable-links.ts"; | |
| 34 | ||
| 35 | import type { Question } from "@/q+a/models/Question.ts"; | |
| 36 | import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts"; | |
| 37 | import { CloverMarkdown } from "@/q+a/clover-markdown"; |
src/q+a/components/clickable-links.client.ts created+126| ... | ... | @@ -0,0 +1,126 @@ |
| 1 | // __esModule | |
| 2 | true | |
| 3 | ||
| 4 | // importDefault | |
| 5 | function importDefault(file, request, nameHint) { | |
| 6 | const imports = getImports(file); | |
| 7 | request = resolveRelativePath(file, request); | |
| 8 | let importDeclaration = imports.get(request); | |
| 9 | ||
| 10 | if (!importDeclaration) { | |
| 11 | imports.set( | |
| 12 | request, | |
| 13 | importDeclaration = file.path.pushContainer( | |
| 14 | "body", | |
| 15 | _compiler.types.importDeclaration([], _compiler.types.stringLiteral(request)) | |
| 16 | )[0] | |
| 17 | ); | |
| 18 | } | |
| 19 | ||
| 20 | if (!nameHint) { | |
| 21 | return; | |
| 22 | } | |
| 23 | ||
| 24 | const specifiers = importDeclaration.get("specifiers"); | |
| 25 | const specifier = specifiers.find((specifier) => | |
| 26 | specifier.isImportDefaultSpecifier() | |
| 27 | ); | |
| 28 | ||
| 29 | if (!specifier) { | |
| 30 | const identifier = file.scope.generateUidIdentifier(nameHint); | |
| 31 | importDeclaration.pushContainer( | |
| 32 | "specifiers", | |
| 33 | _compiler.types.importDefaultSpecifier(identifier) | |
| 34 | ); | |
| 35 | return identifier; | |
| 36 | } | |
| 37 | ||
| 38 | return _compiler.types.identifier(specifier.node.local.name); | |
| 39 | } | |
| 40 | ||
| 41 | // importNamed | |
| 42 | function importNamed(file, request, name, nameHint = name) { | |
| 43 | request = resolveRelativePath(file, request); | |
| 44 | const imports = getImports(file); | |
| 45 | let importDeclaration = imports.get(request); | |
| 46 | ||
| 47 | if (!importDeclaration) { | |
| 48 | imports.set( | |
| 49 | request, | |
| 50 | importDeclaration = file.path.pushContainer( | |
| 51 | "body", | |
| 52 | _compiler.types.importDeclaration([], _compiler.types.stringLiteral(request)) | |
| 53 | )[0] | |
| 54 | ); | |
| 55 | } | |
| 56 | ||
| 57 | const specifiers = importDeclaration.get("specifiers"); | |
| 58 | const specifier = specifiers.find( | |
| 59 | (specifier) => | |
| 60 | specifier.isImportSpecifier() && specifier.node.imported.name === name | |
| 61 | ); | |
| 62 | ||
| 63 | if (!specifier) { | |
| 64 | const identifier = file.scope.generateUidIdentifier(nameHint); | |
| 65 | importDeclaration.pushContainer( | |
| 66 | "specifiers", | |
| 67 | _compiler.types.importSpecifier(identifier, _compiler.types.identifier(name)) | |
| 68 | ); | |
| 69 | return identifier; | |
| 70 | } | |
| 71 | ||
| 72 | return _compiler.types.identifier(specifier.node.local.name); | |
| 73 | } | |
| 74 | ||
| 75 | // importStar | |
| 76 | function importStar(file, request, nameHint) { | |
| 77 | const imports = getImports(file); | |
| 78 | request = resolveRelativePath(file, request); | |
| 79 | let importDeclaration = imports.get(request); | |
| 80 | ||
| 81 | if (!importDeclaration) { | |
| 82 | imports.set( | |
| 83 | request, | |
| 84 | importDeclaration = file.path.pushContainer( | |
| 85 | "body", | |
| 86 | _compiler.types.importDeclaration([], _compiler.types.stringLiteral(request)) | |
| 87 | )[0] | |
| 88 | ); | |
| 89 | } | |
| 90 | ||
| 91 | if (!nameHint) { | |
| 92 | return; | |
| 93 | } | |
| 94 | ||
| 95 | const specifiers = importDeclaration.get("specifiers"); | |
| 96 | const specifier = specifiers.find((specifier) => | |
| 97 | specifier.isImportNamespaceSpecifier() | |
| 98 | ); | |
| 99 | ||
| 100 | if (!specifier) { | |
| 101 | const identifier = file.scope.generateUidIdentifier(nameHint); | |
| 102 | importDeclaration.pushContainer( | |
| 103 | "specifiers", | |
| 104 | _compiler.types.importNamespaceSpecifier(identifier) | |
| 105 | ); | |
| 106 | return identifier; | |
| 107 | } | |
| 108 | ||
| 109 | return _compiler.types.identifier(specifier.node.local.name); | |
| 110 | } | |
| 111 | ||
| 112 | // resolveRelativePath | |
| 113 | function resolveRelativePath(file, request) { | |
| 114 | if (request.startsWith(FS_START)) { | |
| 115 | request = (0, _relativeImportPath.relativeImportPath)(file.opts.filename, request); | |
| 116 | } | |
| 117 | ||
| 118 | if (file.markoOpts.optimize) { | |
| 119 | request = request.replace( | |
| 120 | /(^|\/node-modules\/)marko\/src\//, | |
| 121 | "$1marko/dist/" | |
| 122 | ); | |
| 123 | } | |
| 124 | ||
| 125 | return request; | |
| 126 | } |
src/q+a/edit-frontend.css created+39| ... | ... | @@ -0,0 +1,39 @@ |
| 1 | #edit-grid { | |
| 2 | display: grid; | |
| 3 | grid-template-columns: 1fr 80ch; | |
| 4 | grid-template-rows: 3rem 1fr; | |
| 5 | grid-gap: 1em; | |
| 6 | height: 100vh; | |
| 7 | ||
| 8 | button { | |
| 9 | margin-right: 1rem; | |
| 10 | } | |
| 11 | main { | |
| 12 | padding: 0; | |
| 13 | margin: 0; | |
| 14 | } | |
| 15 | } | |
| 16 | #topleft, #topright { | |
| 17 | padding: 1rem; | |
| 18 | } | |
| 19 | #topleft { | |
| 20 | padding-right: 0rem; | |
| 21 | } | |
| 22 | #topright { | |
| 23 | padding-left: 0rem; | |
| 24 | } | |
| 25 | #preview { | |
| 26 | overflow-y: auto; | |
| 27 | e- { | |
| 28 | margin-top: 0; | |
| 29 | } | |
| 30 | } | |
| 31 | #editor { | |
| 32 | background-color: #303030; | |
| 33 | overflow-y: scroll; | |
| 34 | height: 100%; | |
| 35 | } | |
| 36 | .cm-scroller { | |
| 37 | overflow-y: auto; | |
| 38 | height: 100%; | |
| 39 | } |
src/q+a/edit-frontend.tsx created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | // default | |
| 2 | function prependToMemberExpression(member, prepend) { | |
| 3 | if ((0, _index2.isSuper)(member.object)) { | |
| 4 | throw new Error("Cannot prepend node to super property access (`super.foo`)."); | |
| 5 | } | |
| 6 | member.object = (0, _index.memberExpression)(prepend, member.object); | |
| 7 | return member; | |
| 8 | } |
src/q+a/format.ts created| Binary files /dev/null and b/src/q+a/format.ts differ |
src/q+a/header.client.ts created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | const h1 = document.querySelector("h1")!; | |
| 2 | h1.addEventListener("click", () => { | |
| 3 | h1.classList.toggle("invert"); | |
| 4 | }); |
src/q+a/layout.tsx created+30| ... | ... | @@ -0,0 +1,30 @@ |
| 1 | import "./questions.css"; | |
| 2 | ||
| 3 | export default function QuestionLayout({ children }: { children: any }) { | |
| 4 | addScript("./header.client.ts"); | |
| 5 | return ( | |
| 6 | <main class="qa"> | |
| 7 | <h1> | |
| 8 | <span class="a" style="color:var(--a);--s:#19607d">answers</span> | |
| 9 | <span class="n" style="--s:#999;margin:0 5px">&</span> | |
| 10 | <span class="q" style="color:var(--b);--s:#994253">questions</span> | |
| 11 | </h1> | |
| 12 | {children} | |
| 13 | </main> | |
| 14 | ); | |
| 15 | } | |
| 16 | ||
| 17 | export const theme = { | |
| 18 | fg: "#f0f0f0", | |
| 19 | bg: "#1a1a1a", | |
| 20 | primary: "#f5a9b8", | |
| 21 | }; | |
| 22 | ||
| 23 | export const minimal = { | |
| 24 | default: ({ children }: { children: any }) => ( | |
| 25 | <main class="qa">{children}</main> | |
| 26 | ), | |
| 27 | theme, | |
| 28 | }; | |
| 29 | ||
| 30 | import { addScript } from "#sitegen"; |
src/q+a/models/PendingQuestion.ts created+72| ... | ... | @@ -0,0 +1,72 @@ |
| 1 | import "./Question.ts"; // contains schema | |
| 2 | ||
| 3 | const db = getDb("questions.sqlite"); | |
| 4 | ||
| 5 | export interface PendingQuestionData { | |
| 6 | date: string; | |
| 7 | prompt: string; | |
| 8 | sourceName: string | "unknown"; | |
| 9 | sourceLocation: string | "unknown"; | |
| 10 | sourceVPN: string | null; | |
| 11 | } | |
| 12 | ||
| 13 | /** PendingQuestion is actually a `Question` but with type == .pending */ | |
| 14 | export class PendingQuestion { | |
| 15 | /** | |
| 16 | * questions are identified by a 'minute identifier' | |
| 17 | * which is time / 60000 (minutes since epoch) | |
| 18 | */ | |
| 19 | qmid!: number; | |
| 20 | private type!: QuestionType.pending; | |
| 21 | private text!: string; | |
| 22 | private cached_json?: PendingQuestionData; | |
| 23 | ||
| 24 | // -- instance ops -- | |
| 25 | ||
| 26 | /** "https://paperclover.net/q+a/${id}" - #YYMMDDHHMM */ | |
| 27 | get id(): string { | |
| 28 | return formatQuestionId(this.date); | |
| 29 | } | |
| 30 | get date(): Date { | |
| 31 | return new Date(this.qmid * 1000 * 60); | |
| 32 | } | |
| 33 | get data(): PendingQuestionData { | |
| 34 | assert(this.type === QuestionType.pending); | |
| 35 | return this.cached_json ??= JSON.parse(this.text); | |
| 36 | } | |
| 37 | get prompt() { | |
| 38 | return this.data.prompt; | |
| 39 | } | |
| 40 | get sourceName() { | |
| 41 | return this.data.sourceName; | |
| 42 | } | |
| 43 | get sourceLocation() { | |
| 44 | return this.data.sourceLocation; | |
| 45 | } | |
| 46 | get sourceVPN() { | |
| 47 | return this.data.sourceVPN; | |
| 48 | } | |
| 49 | ||
| 50 | // -- static ops -- | |
| 51 | ||
| 52 | static getAll() { | |
| 53 | return getAllQuery.iter(); | |
| 54 | } | |
| 55 | static getByDate(timestamp: Date) { | |
| 56 | const qmid = Math.floor(timestamp.getTime() / 1000 / 60); | |
| 57 | return getByDateQuery.get(qmid); | |
| 58 | } | |
| 59 | } | |
| 60 | ||
| 61 | const getAllQuery = db.prepare(` | |
| 62 | SELECT * FROM questions WHERE type = ${QuestionType.pending} ORDER BY qmid DESC | |
| 63 | `).as(PendingQuestion); | |
| 64 | ||
| 65 | const getByDateQuery = db.prepare<[qmid: number]>(` | |
| 66 | SELECT * FROM questions WHERE qmid = ? AND type = ${QuestionType.pending} LIMIT 1 | |
| 67 | `).as(PendingQuestion); | |
| 68 | ||
| 69 | import { getDb } from "#sqlite"; | |
| 70 | import assert from "node:assert"; | |
| 71 | import { QuestionType } from "./Question.ts"; | |
| 72 | import { formatQuestionId } from "../format.ts"; |
src/q+a/models/Question.ts created+118| ... | ... | @@ -0,0 +1,118 @@ |
| 1 | const db = getDb("questions.sqlite"); | |
| 2 | db.table( | |
| 3 | "questions", | |
| 4 | ` | |
| 5 | create table questions ( | |
| 6 | qmid integer not null, | |
| 7 | type integer not null, | |
| 8 | text text not null, | |
| 9 | primary key (qmid) | |
| 10 | ); | |
| 11 | `, | |
| 12 | ); | |
| 13 | ||
| 14 | export enum QuestionType { | |
| 15 | normal = 0, | |
| 16 | reject = 1, | |
| 17 | unused = 2, | |
| 18 | pending = 3, | |
| 19 | } | |
| 20 | ||
| 21 | export class Question { | |
| 22 | /** | |
| 23 | * questions are identified by a 'minute identifier' | |
| 24 | * which is time / 60000 (minutes since epoch) | |
| 25 | */ | |
| 26 | qmid!: number; | |
| 27 | type!: QuestionType; | |
| 28 | text!: string; | |
| 29 | ||
| 30 | // -- instance ops -- | |
| 31 | ||
| 32 | /** "https://paperclover.net/q+a/${id}" - #YYMMDDHHMM */ | |
| 33 | get id(): string { | |
| 34 | return formatQuestionId(this.date); | |
| 35 | } | |
| 36 | get date(): Date { | |
| 37 | return new Date(Number(this.qmid) * 1000 * 60); | |
| 38 | } | |
| 39 | ||
| 40 | // -- static ops -- | |
| 41 | ||
| 42 | /** Create a new Question, rounding the date to the nearest available slot */ | |
| 43 | static create(type: QuestionType, text: string, date?: Date) { | |
| 44 | const qmidWanted = Math.floor((date?.getTime() ?? Date.now()) / 1000 / 60); | |
| 45 | const { qmid } = createQuery.getNonNull({ | |
| 46 | type, | |
| 47 | text, | |
| 48 | qmid: qmidWanted, | |
| 49 | }); | |
| 50 | ||
| 51 | return new Date(qmid * 1000 * 60); | |
| 52 | } | |
| 53 | static getAll() { | |
| 54 | return getAllQuery.iter(); | |
| 55 | } | |
| 56 | static getByDate(timestamp: Date) { | |
| 57 | // Older questions are inserted with second precision, | |
| 58 | // but that data should always be floored. | |
| 59 | const qmid = Math.floor(timestamp.getTime() / 1000 / 60); | |
| 60 | return getByDateQuery.get(qmid); | |
| 61 | } | |
| 62 | static deleteByQmid(qmid: number) { | |
| 63 | return deleteByQmidQuery.run(qmid); | |
| 64 | } | |
| 65 | static rejectByQmid(qmid: number) { | |
| 66 | return rejectByQmidQuery.run(qmid); | |
| 67 | } | |
| 68 | static updateByQmid(qmid: number, text: string, type: QuestionType) { | |
| 69 | return updateByQmidQuery.run({ text, type, qmid }); | |
| 70 | } | |
| 71 | } | |
| 72 | ||
| 73 | // Create a new question with a unique QMID | |
| 74 | const createQuery = db.prepare< | |
| 75 | [{ type: QuestionType; text: string; qmid: number }], | |
| 76 | { qmid: number } | |
| 77 | >(/* SQL */ ` | |
| 78 | WITH RECURSIVE | |
| 79 | timestamp(value) AS ( | |
| 80 | SELECT $qmid | |
| 81 | UNION ALL | |
| 82 | SELECT value + 1 | |
| 83 | FROM timestamp | |
| 84 | WHERE EXISTS (SELECT 1 FROM questions WHERE qmid = value) | |
| 85 | ) | |
| 86 | INSERT INTO questions (type, text, qmid) | |
| 87 | VALUES ($type, $text, ( | |
| 88 | SELECT value FROM timestamp | |
| 89 | WHERE NOT EXISTS (SELECT 1 FROM questions WHERE qmid = value) | |
| 90 | LIMIT 1 | |
| 91 | )) | |
| 92 | RETURNING qmid | |
| 93 | `); | |
| 94 | ||
| 95 | const getAllQuery = db.prepare(/* SQL */ ` | |
| 96 | SELECT * FROM questions WHERE type = ${QuestionType.normal} ORDER BY qmid DESC | |
| 97 | `).as(Question); | |
| 98 | ||
| 99 | const getByDateQuery = db.prepare<[qmid: number]>(` | |
| 100 | SELECT * FROM questions WHERE qmid = ? LIMIT 1 | |
| 101 | `).as(Question); | |
| 102 | ||
| 103 | const deleteByQmidQuery = db.prepare<[qmid: number]>(` | |
| 104 | DELETE FROM questions WHERE qmid = ? | |
| 105 | `); | |
| 106 | ||
| 107 | const rejectByQmidQuery = db.prepare<[qmid: number]>(` | |
| 108 | UPDATE questions SET type = ${QuestionType.reject} WHERE qmid = ? | |
| 109 | `); | |
| 110 | ||
| 111 | const updateByQmidQuery = db.prepare< | |
| 112 | [{ qmid: number; text: string; type: QuestionType }] | |
| 113 | >(` | |
| 114 | UPDATE questions SET text = $text, type = $type WHERE qmid = $qmid | |
| 115 | `); | |
| 116 | ||
| 117 | import { formatQuestionId } from "../format.ts"; | |
| 118 | import { getDb } from "#sqlite"; |
src/q+a/pages/admin.q+a.marko created+23| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | export * as layout from "../layout.tsx"; | |
| 2 | export interface Input { | |
| 3 | admin?: boolean; | |
| 4 | } | |
| 5 | export const meta: Metadata = { | |
| 6 | title: "paper clover q+a", | |
| 7 | description: "ask clover a question", | |
| 8 | }; | |
| 9 | ||
| 10 | <const/inboxSize = PendingQuestion.getAll().length /> | |
| 11 | <if=(inboxSize > 0)> | |
| 12 | <p> | |
| 13 | <strong style={ color: "#ff4b4b" }> | |
| 14 | notice: ${inboxSize} items in | |
| 15 | <a href="/admin/q+a/inbox">inbox</a>! | |
| 16 | </strong> | |
| 17 | </p> | |
| 18 | </> | |
| 19 | ||
| 20 | <MainPage admin /> | |
| 21 | ||
| 22 | import MainPage from "./q+a.marko"; | |
| 23 | import { PendingQuestion } from "@/q+a/models/PendingQuestion.ts"; |
src/q+a/pages/q+a.404.marko created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | export * as layout from "../layout.tsx"; | |
| 2 | export const meta: Metadata = { | |
| 3 | title: "paper clover q+a", | |
| 4 | }; | |
| 5 | ||
| 6 | <h2>sound the alarms</h2> | |
| 7 | <p> | |
| 8 | <a href="/q+a">return to the questions list</a> | |
| 9 | </p> |
src/q+a/pages/q+a.marko created+35| ... | ... | @@ -0,0 +1,35 @@ |
| 1 | export * as layout from "../layout.tsx"; | |
| 2 | ||
| 3 | export interface Input { | |
| 4 | admin?: boolean; | |
| 5 | } | |
| 6 | ||
| 7 | export const meta: Metadata = { | |
| 8 | title: "paper clover q+a", | |
| 9 | description: "ask clover a question", | |
| 10 | }; | |
| 11 | ||
| 12 | <const/{ admin = false } = input /> | |
| 13 | <const/questions = [...Question.getAll()] /> | |
| 14 | ||
| 15 | <form action="/q+a" method="POST"> | |
| 16 | <textarea | |
| 17 | name="text" | |
| 18 | placeholder="ask clover a question..." | |
| 19 | required | |
| 20 | minlength="1" | |
| 21 | maxlength="10000" | |
| 22 | /> | |
| 23 | <div aria-hidden class="title">ask a question</div> | |
| 24 | <button type="submit">send</button> | |
| 25 | <div class="disabled-button">send</div> | |
| 26 | </form> | |
| 27 | <for|q| of=questions> | |
| 28 | <QuestionRender question=q admin=admin />; | |
| 29 | </> | |
| 30 | <footer> | |
| 31 | fun fact: clover has answered ${questions.length} questions | |
| 32 | </footer> | |
| 33 | ||
| 34 | import { Question } from "@/q+a/models/Question.ts"; | |
| 35 | import QuestionRender from '@/q+a/components/Question.marko'; |
src/q+a/question-image.tsx created| Binary files /dev/null and b/src/q+a/question-image.tsx differ |
src/q+a/questions.css created+257| ... | ... | @@ -0,0 +1,257 @@ |
| 1 | @font-face { | |
| 2 | font-family: "recu"; | |
| 3 | src: url("/recqa.woff2") format("woff2"); | |
| 4 | font-weight: 400 1000; | |
| 5 | font-style: normal; | |
| 6 | font-display: swap; | |
| 7 | font-variation-settings: "CASL" 0 1, "MONO" 0 1; | |
| 8 | font-style: oblique -15deg 0deg; | |
| 9 | unicode-range: U+0020-007E; | |
| 10 | } | |
| 11 | @font-face { | |
| 12 | font-family: recu; | |
| 13 | src: url("/recqaexotic.woff2") format("woff2"); | |
| 14 | font-weight: 400 800; | |
| 15 | font-style: normal; | |
| 16 | font-display: swap; | |
| 17 | font-variation-settings: CASL 0.1 0.75, MONO 0 1; | |
| 18 | font-style: oblique -15deg 0deg; | |
| 19 | unicode-range: | |
| 20 | U+00C0-00FF, | |
| 21 | U+00A9, | |
| 22 | U+2190-2193, | |
| 23 | U+2018, | |
| 24 | U+2019, | |
| 25 | U+201C, | |
| 26 | U+201D, | |
| 27 | U+2022, | |
| 28 | U+00A0-00A8, | |
| 29 | U+00AA-00BF, | |
| 30 | U+2194-2199, | |
| 31 | U+0100-017F; | |
| 32 | } | |
| 33 | ||
| 34 | .qa { | |
| 35 | /* --a: #faa719; */ | |
| 36 | /* --b: #ff3a32; */ | |
| 37 | --a: #5bcefa; | |
| 38 | --b: #f5a9b8; | |
| 39 | --as: #278bb3; | |
| 40 | --bs: #b75e70; | |
| 41 | ||
| 42 | main { | |
| 43 | margin: auto; | |
| 44 | max-width: 80ch; | |
| 45 | } | |
| 46 | ||
| 47 | e- { | |
| 48 | --fg: #58ff71; | |
| 49 | --new: #8c78ff; | |
| 50 | display: block; | |
| 51 | color: var(--fg); | |
| 52 | margin: 0rem -1rem; | |
| 53 | display: block; | |
| 54 | padding: 1rem; | |
| 55 | overflow-x: hidden; | |
| 56 | font-variation-settings: "CASL" 1, "MONO" 0; | |
| 57 | } | |
| 58 | ||
| 59 | p, | |
| 60 | q- { | |
| 61 | margin-top: 0rem; | |
| 62 | margin-bottom: 0.6rem; | |
| 63 | display: block; | |
| 64 | } | |
| 65 | ||
| 66 | q- { | |
| 67 | color: white; | |
| 68 | --fg: white; | |
| 69 | font-weight: 600; | |
| 70 | font-size: 1.15em; | |
| 71 | font-variation-settings: "CASL" 0, "MONO" 0; | |
| 72 | } | |
| 73 | ||
| 74 | e-[f] { | |
| 75 | --fg: #8c78ff; | |
| 76 | } | |
| 77 | ||
| 78 | h1 { | |
| 79 | display: flex; | |
| 80 | flex-wrap: wrap; | |
| 81 | cursor: pointer; | |
| 82 | margin-top: 0; | |
| 83 | margin-bottom: 1rem; | |
| 84 | line-height: 1.15em; | |
| 85 | user-select: none; | |
| 86 | -webkit-user-select: none; | |
| 87 | font-weight: 800; | |
| 88 | font-size: 3rem; | |
| 89 | font-weight: 862; | |
| 90 | } | |
| 91 | ||
| 92 | h1 span { | |
| 93 | text-shadow: 1px 1px var(--s), 2px 2px var(--s); | |
| 94 | } | |
| 95 | ||
| 96 | time { | |
| 97 | width: max-content; | |
| 98 | color: #999; | |
| 99 | font-size: 0.9rem; | |
| 100 | line-height: 1.5rem; | |
| 101 | } | |
| 102 | ||
| 103 | e- > a[href] { | |
| 104 | /* reset global.css */ | |
| 105 | text-decoration: none !important; | |
| 106 | font-weight: normal !important; | |
| 107 | ||
| 108 | &:hover time { | |
| 109 | color: var(--a); | |
| 110 | } | |
| 111 | &:hover time:not(.winner)::after { | |
| 112 | content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjRweCI+PHBhdGggZD0iTTQ0MC0yODBIMjgwcS04MyAwLTE0MS41LTU4LjVUODAtNDgwcTAtODMgNTguNS0xNDEuNVQyODAtNjgwaDE2MHY4MEgyODBxLTUwIDAtODUgMzV0LTM1IDg1cTAgNTAgMzUgODV0ODUgMzVoMTYwdjgwWk0zMjAtNDQwdi04MGgzMjB2ODBIMzIwWm0yMDAgMTYwdi04MGgxNjBxNTAgMCA4NS0zNXQzNS04NXEwLTUwLTM1LTg1dC04NS0zNUg1MjB2LTgwaDE2MHE4MyAwIDE0MS41IDU4LjVUODgwLTQ4MHEwIDgzLTU4LjUgMTQxLjVUNjgwLTI4MEg1MjBaIiBmaWxsPSIjNWJjZWZhIi8+PC9zdmc+); | |
| 113 | display: inline-block; | |
| 114 | position: absolute; | |
| 115 | transform: translateY(1px); | |
| 116 | } | |
| 117 | &:active { | |
| 118 | filter: hue-rotate(155deg)brightness(1.3); | |
| 119 | } | |
| 120 | } | |
| 121 | .winner { | |
| 122 | color: #58ff71 !important; | |
| 123 | } | |
| 124 | textarea, button { | |
| 125 | font-family: monospace; | |
| 126 | border: 2px solid black; | |
| 127 | background-color: #242424; | |
| 128 | color: #fff; | |
| 129 | box-sizing: content-box; | |
| 130 | padding: 0.5rem; | |
| 131 | font-size: inherit; | |
| 132 | ||
| 133 | &:focus-visible { | |
| 134 | outline: 2px solid var(--a); | |
| 135 | border-color: var(--a); | |
| 136 | &::placeholder { | |
| 137 | opacity: 0; | |
| 138 | } | |
| 139 | } | |
| 140 | } | |
| 141 | button:disabled, .disabled-button { | |
| 142 | font-family: monospace; | |
| 143 | border: 2px solid #141414; | |
| 144 | background-color: #1a1a1a; | |
| 145 | color: #999; | |
| 146 | box-sizing: content-box; | |
| 147 | padding: 0.5rem; | |
| 148 | font-size: inherit; | |
| 149 | } | |
| 150 | .title { | |
| 151 | position: absolute; | |
| 152 | transform: translateY(-0.5rem); | |
| 153 | font-weight: bold; | |
| 154 | z-index: -1; | |
| 155 | } | |
| 156 | form { | |
| 157 | display: flex; | |
| 158 | flex-direction: column; | |
| 159 | button, .disabled-button { | |
| 160 | margin-top: 0.5rem; | |
| 161 | align-self: flex-start; | |
| 162 | } | |
| 163 | } | |
| 164 | textarea { | |
| 165 | width: 100%; | |
| 166 | max-width: calc(60ch); | |
| 167 | height: 4rem; | |
| 168 | resize: vertical; | |
| 169 | /* Note: safari overrides this */ | |
| 170 | margin-top: 1rem !important; | |
| 171 | &:placeholder-shown:not(:focus) { | |
| 172 | min-height: 0px; | |
| 173 | height: calc(1ch + 0.5rem); | |
| 174 | resize: none; | |
| 175 | margin-top: 0 !important; | |
| 176 | & + .title { | |
| 177 | transform: translateY(0.5rem); | |
| 178 | } | |
| 179 | } | |
| 180 | &:not(:placeholder-shown) ~ .disabled-button, | |
| 181 | &:placeholder-shown ~ button { | |
| 182 | display: none; | |
| 183 | } | |
| 184 | } | |
| 185 | .dead { | |
| 186 | color: var(--bg); | |
| 187 | color: rgb(from var(--bg) r g b / 0.6); | |
| 188 | background-color: var(--fg); | |
| 189 | padding: 0.04rem 0.5rem; | |
| 190 | font-size: 82%; | |
| 191 | text-decoration: line-through; | |
| 192 | text-decoration-thickness: 3px; | |
| 193 | text-decoration-color: var(--bg); | |
| 194 | white-space: nowrap; | |
| 195 | } | |
| 196 | .question-ref, .artifact { | |
| 197 | --o: #5bcefa; | |
| 198 | background-color: #5bcefa33; | |
| 199 | padding: 0.15em 0.2em; | |
| 200 | border-radius: 4px; | |
| 201 | color: var(--o); | |
| 202 | font-size: 90%; | |
| 203 | white-space: nowrap; | |
| 204 | ||
| 205 | &::before { | |
| 206 | transform: translateY(0.2em); | |
| 207 | display: inline-block; | |
| 208 | margin-right: 3px; | |
| 209 | } | |
| 210 | ||
| 211 | &:hover { | |
| 212 | background-color: #5bcefa66; | |
| 213 | text-decoration: none; | |
| 214 | } | |
| 215 | ||
| 216 | &:active { | |
| 217 | background-color: var(--o); | |
| 218 | color: var(--bg); | |
| 219 | &::before { | |
| 220 | filter: brightness(0.1); | |
| 221 | } | |
| 222 | } | |
| 223 | } | |
| 224 | .question-ref::before { | |
| 225 | content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIxNnB4IiBmaWxsPSIjNWJjZWZhIj48cGF0aCBkPSJNODAtODB2LTcyMHEwLTMzIDIzLjUtNTYuNVQxNjAtODgwaDY0MHEzMyAwIDU2LjUgMjMuNVQ4ODAtODAwdjQ4MHEwIDMzLTIzLjUgNTYuNVQ4MDAtMjQwSDI0MEw4MC04MFoiLz48L3N2Zz4=); | |
| 226 | } | |
| 227 | .artifact-game::before { | |
| 228 | content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMThweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIxOHB4IiBmaWxsPSIjZmE1YjViIj48cGF0aCBkPSJNMTg4LTI0MHEtNDQgMC03MS41LTMzLjVUOTctMzUwbDU3LTI5N3E4LTQyIDQwLjg1LTY5LjVUMjcyLTc0NGg0MTZxNDQuMyAwIDc3LjE1IDI3LjVRNzk4LTY4OSA4MDYtNjQ2bDU3IDI5N3E4IDQzLTIwIDc2dC03MiAzM3EtMTYgMC0zMS00LjVUNzE0LTI2MGwtOTUtNzZIMzQxbC05NSA3NnEtMTEgMTEtMjYuNSAxNS41VDE4OC0yNDBabTQ3MS43OS0yMTZxMTUuMjEgMCAyNS43MS0xMC4yOXQxMC41LTI1LjVxMC0xNS4yMS0xMC4yOS0yNS43MXQtMjUuNS0xMC41cS0xNS4yMSAwLTI1LjcxIDEwLjI5dC0xMC41IDI1LjVxMCAxNS4yMSAxMC4yOSAyNS43MXQyNS41IDEwLjVabS03Mi05NnExNS4yMSAwIDI1LjcxLTEwLjI5dDEwLjUtMjUuNXEwLTE1LjIxLTEwLjI5LTI1LjcxdC0yNS41LTEwLjVxLTE1LjIxIDAtMjUuNzEgMTAuMjl0LTEwLjUgMjUuNXEwIDE1LjIxIDEwLjI5IDI1LjcxdDI1LjUgMTAuNVpNMzM2LTQ1Nmg0OHYtNjBoNjB2LTQ4aC02MHYtNjBoLTQ4djYwaC02MHY0OGg2MHY2MFoiLz48L3N2Zz4=); | |
| 229 | } | |
| 230 | .artifact-music::before { | |
| 231 | content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMThweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIxOHB4IiBmaWxsPSIjZmE1YjViIj48cGF0aCBkPSJNNDA4LTE0NHEtNjAgMC0xMDItNDJ0LTQyLTEwMnEwLTYwIDQyLTEwMnQxMDItNDJxMjAgMCAzOCA1LjV0MzQgMTQuNXYtNDA0aDIxNnYxNDRINTUydjM4NHEwIDYwLTQyIDEwMnQtMTAyIDQyWiIvPjwvc3ZnPg==); | |
| 232 | } | |
| 233 | .artifact { | |
| 234 | --o: #fa5b5b; | |
| 235 | background-color: #fa5b5b33; | |
| 236 | &:hover { | |
| 237 | background-color: #fa5b5b66; | |
| 238 | } | |
| 239 | } | |
| 240 | } | |
| 241 | @media screen and (min-width: 550px) { | |
| 242 | .qa h1 span { | |
| 243 | transition: transform 120ms cubic-bezier(0.4, 0, 0.2, 1); | |
| 244 | } | |
| 245 | ||
| 246 | .invert { | |
| 247 | .a { | |
| 248 | transform: translateX(280px); | |
| 249 | } | |
| 250 | .n { | |
| 251 | transform: translateX(40px); | |
| 252 | } | |
| 253 | .q { | |
| 254 | transform: translateX(calc(-100%)); | |
| 255 | } | |
| 256 | } | |
| 257 | } |
src/q+a/simple-markdown.ts created+867| ... | ... | @@ -0,0 +1,867 @@ |
| 1 | // @ts-nocheck | |
| 2 | // # Simple-Markdown Core | |
| 3 | // | |
| 4 | // This is a fork of Khan-academy's Simple-Markdown[1], initially forked in 2022 | |
| 5 | // to add Svelte support[2], and used for paper clover's q+a markdown flavor. | |
| 6 | // | |
| 7 | // 1: https://github.com/Khan/perseus/tree/main/packages/simple-markdown/src | |
| 8 | // 2: https://github.com/paperclover/svelte-simple-markdown | |
| 9 | export type Rules = Record<string, ParserRule>; | |
| 10 | export interface ParserRule { | |
| 11 | name: string; | |
| 12 | match: MatchFunction; | |
| 13 | parse: ParseFunction; | |
| 14 | quality?: QualityFunction; | |
| 15 | } | |
| 16 | ||
| 17 | export class RuleList extends Array<ParserRule> { | |
| 18 | constructor(input?: ArrayLike<ParserRule>) { | |
| 19 | super(); | |
| 20 | if (input) { | |
| 21 | this.push(...Array.from(input)); | |
| 22 | } | |
| 23 | } | |
| 24 | ||
| 25 | insertBefore(rule: string, newRule: ParserRule): void { | |
| 26 | const index = this.findIndex((r) => r.name === rule); | |
| 27 | if (index === -1) { | |
| 28 | throw new Error(`Rule ${rule} not found`); | |
| 29 | } | |
| 30 | this.splice(index, 0, newRule); | |
| 31 | } | |
| 32 | ||
| 33 | insertAfter(rule: string, newRule: ParserRule): void { | |
| 34 | const index = this.findIndex((r) => r.name === rule); | |
| 35 | if (index === -1) { | |
| 36 | throw new Error(`Rule ${rule} not found`); | |
| 37 | } | |
| 38 | this.splice(index + 1, 0, newRule); | |
| 39 | } | |
| 40 | ||
| 41 | toRuleObject(): Record<string, ParserRule> { | |
| 42 | const result: Record<string, ParserRule> = {}; | |
| 43 | this.forEach((rule) => { | |
| 44 | result[rule.name] = rule; | |
| 45 | }); | |
| 46 | return result; | |
| 47 | } | |
| 48 | ||
| 49 | add(rule: ParserRule): void { | |
| 50 | this.push(rule); | |
| 51 | } | |
| 52 | ||
| 53 | get(rule: string): ParserRule | undefined { | |
| 54 | return this.find((r) => r.name === rule); | |
| 55 | } | |
| 56 | ||
| 57 | remove(rule: string): void { | |
| 58 | const index = this.findIndex((r) => r.name === rule); | |
| 59 | if (index === -1) { | |
| 60 | throw new Error(`Rule ${rule} not found`); | |
| 61 | } | |
| 62 | this.splice(index, 1); | |
| 63 | } | |
| 64 | ||
| 65 | clone() { | |
| 66 | return new RuleList(this); | |
| 67 | } | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * Creates a parser for a given set of rules, with the precedence | |
| 72 | * specified as a list of rules. | |
| 73 | * | |
| 74 | * @param rules | |
| 75 | * an object containing | |
| 76 | * rule type -> {match, order, parse} objects | |
| 77 | * (lower order is higher precedence) | |
| 78 | * @param [defaultState] | |
| 79 | * | |
| 80 | * @returns | |
| 81 | * The resulting parse function, with the following parameters: | |
| 82 | * @source: the input source string to be parsed | |
| 83 | * @state: an optional object to be threaded through parse | |
| 84 | * calls. Allows clients to add stateful operations to | |
| 85 | * parsing, such as keeping track of how many levels deep | |
| 86 | * some nesting is. For an example use-case, see passage-ref | |
| 87 | * parsing in src/widgets/passage/passage-markdown.jsx | |
| 88 | */ | |
| 89 | export function createParser( | |
| 90 | ruleListInput: RuleList, | |
| 91 | defaultState: Partial<ParserState> = {}, | |
| 92 | ) { | |
| 93 | let rules = ruleListInput.toRuleObject(); | |
| 94 | let ruleList = Object.keys(rules); | |
| 95 | ||
| 96 | let latestState: ParserState; | |
| 97 | ||
| 98 | let nestedParse = function (source: string, state?: ParserState) { | |
| 99 | let result: ASTNode[] = []; | |
| 100 | state = state || latestState; | |
| 101 | latestState = state; | |
| 102 | ||
| 103 | while (source) { | |
| 104 | // store the best match, it's rule, and quality: | |
| 105 | let ruleType = null; | |
| 106 | let rule = null; | |
| 107 | let capture = null; | |
| 108 | let quality = NaN; // loop control variables: | |
| 109 | ||
| 110 | let i = 0; | |
| 111 | let currRuleType = ruleList[0]; | |
| 112 | ||
| 113 | let currRule = rules[currRuleType]; | |
| 114 | ||
| 115 | do { | |
| 116 | let currCapture = currRule.match(source, state); | |
| 117 | ||
| 118 | if (currCapture) { | |
| 119 | let currQuality = currRule.quality | |
| 120 | ? currRule.quality(currCapture, state) | |
| 121 | : 0; | |
| 122 | ||
| 123 | // This should always be true the first time because | |
| 124 | // the initial quality is NaN (that's why there's the | |
| 125 | // condition negation). | |
| 126 | if (!(currQuality <= quality)) { | |
| 127 | ruleType = currRuleType; | |
| 128 | rule = currRule; | |
| 129 | capture = currCapture; | |
| 130 | quality = currQuality; | |
| 131 | } | |
| 132 | } | |
| 133 | ||
| 134 | // Move on to the next item. | |
| 135 | // Note that this makes `currRule` be the next item | |
| 136 | i++; | |
| 137 | currRuleType = ruleList[i]; | |
| 138 | currRule = rules[currRuleType]; | |
| 139 | } while ( | |
| 140 | // keep looping while we're still within the ruleList | |
| 141 | currRule && | |
| 142 | // if we don't have a match yet, continue | |
| 143 | (!capture || | |
| 144 | // or if we have a match, but the next rule is | |
| 145 | // at the same order, and has a quality measurement | |
| 146 | // functions, then this rule must have a quality | |
| 147 | // measurement function (since they are sorted before | |
| 148 | // those without), and we need to check if there is | |
| 149 | // a better quality match | |
| 150 | currRule.quality) | |
| 151 | ); | |
| 152 | ||
| 153 | if (!rule || !capture || !ruleType) { | |
| 154 | throw new Error( | |
| 155 | "Could not find a matching rule for the below " + | |
| 156 | "content. The rule with highest `order` should " + | |
| 157 | "always match content provided to it. Check " + | |
| 158 | "the definition of `match` for '" + | |
| 159 | ruleList[ruleList.length - 1] + | |
| 160 | "'. It seems to not match the following source:\n" + | |
| 161 | source, | |
| 162 | ); | |
| 163 | } | |
| 164 | ||
| 165 | if (capture.index) { | |
| 166 | // If present and non-zero, i.e. a non-^ regexp result: | |
| 167 | throw new Error( | |
| 168 | "`match` must return a capture starting at index 0 " + | |
| 169 | "(the current parse index). Did you forget a ^ at the " + | |
| 170 | "start of the RegExp?", | |
| 171 | ); | |
| 172 | } | |
| 173 | ||
| 174 | let parsed = rule.parse(capture, nestedParse, state); | |
| 175 | ||
| 176 | // We maintain the same object here so that rules can | |
| 177 | // store references to the objects they return and | |
| 178 | // modify them later. (oops sorry! but this adds a lot | |
| 179 | // of power--see reflinks.) | |
| 180 | ||
| 181 | // We also let rules override the default type of | |
| 182 | // their parsed node if they would like to, so that | |
| 183 | // there can be a single output function for all links, | |
| 184 | // even if there are several rules to parse them. | |
| 185 | if (!parsed.type) { | |
| 186 | parsed.type = ruleType; | |
| 187 | } | |
| 188 | ||
| 189 | // Collapse text nodes | |
| 190 | if ( | |
| 191 | parsed.type === "text" && result[result.length - 1]?.type === "text" | |
| 192 | ) { | |
| 193 | result[result.length - 1].content += parsed.content; | |
| 194 | } else { | |
| 195 | result.push(parsed as ASTNode); | |
| 196 | } | |
| 197 | ||
| 198 | state.prevCapture = capture; | |
| 199 | source = source.substring(state.prevCapture[0].length); | |
| 200 | } | |
| 201 | ||
| 202 | return result; | |
| 203 | }; | |
| 204 | ||
| 205 | let outerParse = function ( | |
| 206 | source: string, | |
| 207 | state: ParserState = { inline: false }, | |
| 208 | ) { | |
| 209 | latestState = populateInitialState(state, defaultState); | |
| 210 | ||
| 211 | if (!latestState.inline && !latestState.disableAutoBlockNewlines) { | |
| 212 | source = source + "\n\n"; | |
| 213 | } | |
| 214 | ||
| 215 | // We store the previous capture so that match functions can | |
| 216 | // use some limited amount of lookbehind. Lists use this to | |
| 217 | // ensure they don't match arbitrary '- ' or '* ' in inline | |
| 218 | // text (see the list rule for more information). This stores | |
| 219 | // the full regex capture object, if there is one. | |
| 220 | latestState.prevCapture = undefined; | |
| 221 | return nestedParse(preprocess(source), latestState); | |
| 222 | }; | |
| 223 | ||
| 224 | return outerParse; | |
| 225 | } | |
| 226 | ||
| 227 | type Multiple<T> = T | T[]; | |
| 228 | type Nullable<T> = T | null | undefined; | |
| 229 | ||
| 230 | export type MatchFunction = ( | |
| 231 | source: string, | |
| 232 | state: ParserState, | |
| 233 | ) => Nullable<RegExpMatchArray>; | |
| 234 | ||
| 235 | export type Parser = (source: string, state?: ParserState) => ASTNode[]; | |
| 236 | ||
| 237 | export type ParseFunction = ( | |
| 238 | source: RegExpMatchArray, | |
| 239 | nestedParse: Parser, | |
| 240 | state: ParserState, | |
| 241 | ) => TypeOptionalASTNode; | |
| 242 | ||
| 243 | export type QualityFunction = ( | |
| 244 | capture: RegExpMatchArray, | |
| 245 | state: ParserState, | |
| 246 | ) => number; | |
| 247 | ||
| 248 | export interface ParserState { | |
| 249 | inline: boolean; | |
| 250 | prevCapture?: RegExpMatchArray; | |
| 251 | [key: string]: any; | |
| 252 | } | |
| 253 | ||
| 254 | export interface ASTNode { | |
| 255 | type: string; | |
| 256 | content?: ASTNode[] | string; | |
| 257 | [key: string]: any; | |
| 258 | } | |
| 259 | ||
| 260 | export type TypeOptionalASTNode = Omit<ASTNode, "type"> & { type?: string }; | |
| 261 | ||
| 262 | export interface RefNode { | |
| 263 | type: string; | |
| 264 | content?: Multiple<ASTNode>; | |
| 265 | target?: string; | |
| 266 | title?: string; | |
| 267 | alt?: string; | |
| 268 | } | |
| 269 | ||
| 270 | /** Creates a match function for an inline scoped element from a regex */ | |
| 271 | export function inlineRegex(regex: RegExp): MatchFunction { | |
| 272 | return (source, state) => { | |
| 273 | if (state.inline) { | |
| 274 | return regex.exec(source); | |
| 275 | } else { | |
| 276 | return null; | |
| 277 | } | |
| 278 | }; | |
| 279 | } | |
| 280 | ||
| 281 | /** Creates a match function for a block scoped element from a regex */ | |
| 282 | export function blockRegex(regex: RegExp): MatchFunction { | |
| 283 | return (source, state) => { | |
| 284 | if (state.inline) { | |
| 285 | return null; | |
| 286 | } else { | |
| 287 | return regex.exec(source); | |
| 288 | } | |
| 289 | }; | |
| 290 | } | |
| 291 | ||
| 292 | /** Creates a match function from a regex, ignoring block/inline scope */ | |
| 293 | export function anyScopeRegex(regex: RegExp): MatchFunction { | |
| 294 | return (source) => { | |
| 295 | return regex.exec(source); | |
| 296 | }; | |
| 297 | } | |
| 298 | ||
| 299 | const UNESCAPE_URL_R = /\\([^0-9A-Za-z\s])/g; | |
| 300 | export function unescapeUrl(rawUrlString: string) { | |
| 301 | return rawUrlString.replace(UNESCAPE_URL_R, "$1"); | |
| 302 | } | |
| 303 | ||
| 304 | /** | |
| 305 | * Parse some content with the parser `parse`, with state.inline | |
| 306 | * set to true. Useful for block elements; not generally necessary | |
| 307 | * to be used by inline elements (where state.inline is already true. | |
| 308 | */ | |
| 309 | export function parseInline( | |
| 310 | parse: Parser, | |
| 311 | content: string, | |
| 312 | state: ParserState, | |
| 313 | ) { | |
| 314 | const isCurrentlyInline = state.inline || false; | |
| 315 | state.inline = true; | |
| 316 | const result = parse(content, state); | |
| 317 | state.inline = isCurrentlyInline; | |
| 318 | return result; | |
| 319 | } | |
| 320 | ||
| 321 | export function parseBlock(parse: Parser, content: string, state: ParserState) { | |
| 322 | const isCurrentlyInline = state.inline || false; | |
| 323 | state.inline = false; | |
| 324 | const result = parse(content + "\n\n", state); | |
| 325 | state.inline = isCurrentlyInline; | |
| 326 | return result; | |
| 327 | } | |
| 328 | ||
| 329 | export function parseCaptureInline( | |
| 330 | capture: RegExpMatchArray, | |
| 331 | parse: Parser, | |
| 332 | state: ParserState, | |
| 333 | ) { | |
| 334 | return { | |
| 335 | content: parseInline(parse, capture[1], state), | |
| 336 | }; | |
| 337 | } | |
| 338 | ||
| 339 | export function ignoreCapture() { | |
| 340 | return {}; | |
| 341 | } | |
| 342 | ||
| 343 | export function sanitizeUrl(url?: string) { | |
| 344 | if (url == null) { | |
| 345 | return null; | |
| 346 | } | |
| 347 | try { | |
| 348 | const prot = new URL(url, "https://localhost").protocol; | |
| 349 | if ( | |
| 350 | prot.indexOf("javascript:") === 0 || | |
| 351 | prot.indexOf("vbscript:") === 0 || | |
| 352 | prot.indexOf("data:") === 0 | |
| 353 | ) { | |
| 354 | return null; | |
| 355 | } | |
| 356 | } catch (e) { | |
| 357 | // invalid URLs should throw a TypeError | |
| 358 | // see for instance: `new URL("");` | |
| 359 | return null; | |
| 360 | } | |
| 361 | return url; | |
| 362 | } | |
| 363 | ||
| 364 | const CR_NEWLINE_R = /\r\n?/g; | |
| 365 | const TAB_R = /\t/g; | |
| 366 | const FORMFEED_R = /\f/g; | |
| 367 | ||
| 368 | /** | |
| 369 | * Turn various whitespace into easy-to-process whitespace | |
| 370 | */ | |
| 371 | function preprocess(source: string) { | |
| 372 | return source.replace(CR_NEWLINE_R, "\n").replace(FORMFEED_R, "").replace( | |
| 373 | TAB_R, | |
| 374 | " ", | |
| 375 | ); | |
| 376 | } | |
| 377 | ||
| 378 | function populateInitialState( | |
| 379 | givenState: Partial<ParserState>, | |
| 380 | defaultState: Partial<ParserState>, | |
| 381 | ) { | |
| 382 | let state = givenState || {}; | |
| 383 | ||
| 384 | for (let prop in defaultState) { | |
| 385 | if (Object.prototype.hasOwnProperty.call(defaultState, prop)) { | |
| 386 | state[prop] = defaultState[prop]; | |
| 387 | } | |
| 388 | } | |
| 389 | ||
| 390 | return state as ParserState; | |
| 391 | } | |
| 392 | ||
| 393 | // recognize a `*` `-`, `+`, `1.`, `2.`... list bullet | |
| 394 | const LIST_BULLET = "(?:[*+-]|\\d+\\.)"; | |
| 395 | ||
| 396 | // recognize the start of a list item: | |
| 397 | // leading space plus a bullet plus a space (` * `) | |
| 398 | const LIST_ITEM_PREFIX = "( *)(" + LIST_BULLET + ") +"; | |
| 399 | const LIST_ITEM_PREFIX_R = new RegExp("^" + LIST_ITEM_PREFIX); | |
| 400 | ||
| 401 | // recognize an individual list item: | |
| 402 | // * hi | |
| 403 | // this is part of the same item | |
| 404 | // | |
| 405 | // as is this, which is a new paragraph in the same item | |
| 406 | // | |
| 407 | // * but this is not part of the same item | |
| 408 | const LIST_ITEM_R = new RegExp( | |
| 409 | LIST_ITEM_PREFIX + "[^\\n]*(?:\\n" + "(?!\\1" + LIST_BULLET + | |
| 410 | " )[^\\n]*)*(\n|$)", | |
| 411 | "gm", | |
| 412 | ); | |
| 413 | const BLOCK_END_R = /\n{2,}$/; | |
| 414 | const INLINE_CODE_ESCAPE_BACKTICKS_R = /^ (?= *`)|(` *) $/g; | |
| 415 | ||
| 416 | // recognize the end of a paragraph block inside a list item: | |
| 417 | // two or more newlines at end end of the item | |
| 418 | const LIST_BLOCK_END_R = BLOCK_END_R; | |
| 419 | const LIST_ITEM_END_R = / *\n+$/; | |
| 420 | ||
| 421 | // check whether a list item has paragraphs: if it does, | |
| 422 | // we leave the newlines at the end | |
| 423 | const LIST_R = new RegExp( | |
| 424 | "^( *)(" + | |
| 425 | LIST_BULLET + | |
| 426 | ") " + | |
| 427 | "[\\s\\S]+?(?:\n{2,}(?! )" + | |
| 428 | "(?!\\1" + | |
| 429 | LIST_BULLET + | |
| 430 | " )\\n*" + | |
| 431 | // the \\s*$ here is so that we can parse the inside of nested | |
| 432 | // lists, where our content might end before we receive two `\n`s | |
| 433 | "|\\s*\n*$)", | |
| 434 | ); | |
| 435 | const LIST_LOOKBEHIND_R = /(?:^|\n)( *)$/; | |
| 436 | ||
| 437 | const TABLES = (function () { | |
| 438 | const TABLE_ROW_SEPARATOR_TRIM = /^ *\| *| *\| *$/g; | |
| 439 | const TABLE_CELL_END_TRIM = / *$/; | |
| 440 | const TABLE_RIGHT_ALIGN = /^ *-+: *$/; | |
| 441 | const TABLE_CENTER_ALIGN = /^ *:-+: *$/; | |
| 442 | const TABLE_LEFT_ALIGN = /^ *:-+ *$/; // TODO: This needs a real type | |
| 443 | ||
| 444 | const parseTableAlignCapture = (alignCapture: string) => { | |
| 445 | if (TABLE_RIGHT_ALIGN.test(alignCapture)) { | |
| 446 | return "right"; | |
| 447 | } else if (TABLE_CENTER_ALIGN.test(alignCapture)) { | |
| 448 | return "center"; | |
| 449 | } else if (TABLE_LEFT_ALIGN.test(alignCapture)) { | |
| 450 | return "left"; | |
| 451 | } else { | |
| 452 | return null; | |
| 453 | } | |
| 454 | }; | |
| 455 | ||
| 456 | const parseTableAlign = (source: string, trimEndSeparators: boolean) => { | |
| 457 | if (trimEndSeparators) { | |
| 458 | source = source.replace(TABLE_ROW_SEPARATOR_TRIM, ""); | |
| 459 | } | |
| 460 | ||
| 461 | const alignText = source.trim().split("|"); | |
| 462 | return alignText.map(parseTableAlignCapture); | |
| 463 | }; | |
| 464 | ||
| 465 | const parseTableRow = ( | |
| 466 | source: string, | |
| 467 | parse: Parser, | |
| 468 | state: ParserState, | |
| 469 | trimEndSeparators: boolean, | |
| 470 | ) => { | |
| 471 | const prevInTable = state.inTable; | |
| 472 | state.inTable = true; | |
| 473 | const tableRow = parse(source.trim(), state); | |
| 474 | state.inTable = prevInTable; | |
| 475 | const cells: ASTNode[][] = [[]]; | |
| 476 | tableRow.forEach(function (node, i) { | |
| 477 | if (node.type === "tableSeparator") { | |
| 478 | // Filter out empty table separators at the start/end: | |
| 479 | if (!trimEndSeparators || (i !== 0 && i !== tableRow.length - 1)) { | |
| 480 | // Split the current row: | |
| 481 | cells.push([]); | |
| 482 | } | |
| 483 | } else { | |
| 484 | if ( | |
| 485 | typeof node.content === "string" && | |
| 486 | (tableRow[i + 1] == null || tableRow[i + 1].type === "tableSeparator") | |
| 487 | ) { | |
| 488 | node.content = node.content.replace(TABLE_CELL_END_TRIM, ""); | |
| 489 | } | |
| 490 | ||
| 491 | cells[cells.length - 1].push(node); | |
| 492 | } | |
| 493 | }); | |
| 494 | return cells; | |
| 495 | }; | |
| 496 | ||
| 497 | /** | |
| 498 | * @param {string} source | |
| 499 | * @param {SimpleMarkdown.Parser} parse | |
| 500 | * @param {SimpleMarkdown.State} state | |
| 501 | * @param {boolean} trimEndSeparators | |
| 502 | * @returns {SimpleMarkdown.ASTNode[][]} | |
| 503 | */ | |
| 504 | const parseTableCells = function ( | |
| 505 | source: string, | |
| 506 | parse: Parser, | |
| 507 | state: ParserState, | |
| 508 | trimEndSeparators: boolean, | |
| 509 | ) { | |
| 510 | const rowsText = source.trim().split("\n"); | |
| 511 | return rowsText.map(function (rowText) { | |
| 512 | return parseTableRow(rowText, parse, state, trimEndSeparators); | |
| 513 | }); | |
| 514 | }; | |
| 515 | ||
| 516 | /** | |
| 517 | * @param {boolean} trimEndSeparators | |
| 518 | * @returns {SimpleMarkdown.SingleNodeParseFunction} | |
| 519 | */ | |
| 520 | const parseTable = function (trimEndSeparators: boolean) { | |
| 521 | return function ( | |
| 522 | capture: RegExpMatchArray, | |
| 523 | parse: Parser, | |
| 524 | state: ParserState, | |
| 525 | ) { | |
| 526 | state.inline = true; | |
| 527 | const header = parseTableRow(capture[1], parse, state, trimEndSeparators); | |
| 528 | const align = parseTableAlign(capture[2], trimEndSeparators); | |
| 529 | const cells = parseTableCells( | |
| 530 | capture[3], | |
| 531 | parse, | |
| 532 | state, | |
| 533 | trimEndSeparators, | |
| 534 | ); | |
| 535 | state.inline = false; | |
| 536 | return { | |
| 537 | type: "table", | |
| 538 | header: header, | |
| 539 | align: align, | |
| 540 | cells: cells, | |
| 541 | }; | |
| 542 | }; | |
| 543 | }; | |
| 544 | ||
| 545 | return { | |
| 546 | parseTable: parseTable(true), | |
| 547 | parseNpTable: parseTable(false), | |
| 548 | TABLE_REGEX: /^ *(\|.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/, | |
| 549 | NPTABLE_REGEX: | |
| 550 | /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, | |
| 551 | }; | |
| 552 | })(); | |
| 553 | ||
| 554 | const LINK_INSIDE = "(?:\\[[^\\]]*\\]|[^\\[\\]]|\\](?=[^\\[]*\\]))*"; | |
| 555 | const LINK_HREF_AND_TITLE = | |
| 556 | "\\s*<?((?:\\([^)]*\\)|[^\\s\\\\]|\\\\.)*?)>?(?:\\s+['\"]([\\s\\S]*?)['\"])?\\s*"; | |
| 557 | const AUTOLINK_MAILTO_CHECK_R = /mailto:/i; | |
| 558 | ||
| 559 | function parseRef( | |
| 560 | capture: RegExpMatchArray, | |
| 561 | state: ParserState, | |
| 562 | refNode: RefNode, | |
| 563 | ) { | |
| 564 | const ref = (capture[2] || capture[1]).replace(/\s+/g, " ").toLowerCase(); | |
| 565 | ||
| 566 | // We store information about previously seen defs on | |
| 567 | // state._defs (_ to deconflict with client-defined | |
| 568 | // state). If the def for this reflink/refimage has | |
| 569 | // already been seen, we can use its target/source | |
| 570 | // and title here: | |
| 571 | if (state._defs && state._defs[ref]) { | |
| 572 | const def = state._defs[ref]; | |
| 573 | ||
| 574 | // `refNode` can be a link or an image. Both use | |
| 575 | // target and title properties. | |
| 576 | refNode.target = def.target; | |
| 577 | refNode.title = def.title; | |
| 578 | } | |
| 579 | ||
| 580 | // In case we haven't seen our def yet (or if someone | |
| 581 | // overwrites that def later on), we add this node | |
| 582 | // to the list of ref nodes for that def. Then, when | |
| 583 | // we find the def, we can modify this link/image AST | |
| 584 | // node :). | |
| 585 | // I'm sorry. | |
| 586 | state._refs = state._refs || {}; | |
| 587 | state._refs[ref] = state._refs[ref] || []; | |
| 588 | ||
| 589 | state._refs[ref].push(refNode); | |
| 590 | ||
| 591 | return refNode; | |
| 592 | } | |
| 593 | ||
| 594 | export const defaultRules = new RuleList(); | |
| 595 | { | |
| 596 | defaultRules.add({ | |
| 597 | name: "heading", | |
| 598 | match: blockRegex(/^ *(#{1,6})([^\n]+?)#* *(?:\n *)+\n/), | |
| 599 | parse: function (capture, parse, state) { | |
| 600 | return { | |
| 601 | level: capture[1].length, | |
| 602 | content: parseInline(parse, capture[2].trim(), state), | |
| 603 | }; | |
| 604 | }, | |
| 605 | }); | |
| 606 | ||
| 607 | defaultRules.add({ | |
| 608 | name: "nptable", | |
| 609 | match: blockRegex(TABLES.NPTABLE_REGEX), | |
| 610 | parse: TABLES.parseNpTable, | |
| 611 | }); | |
| 612 | ||
| 613 | defaultRules.add({ | |
| 614 | name: "lheading", | |
| 615 | match: blockRegex(/^([^\n]+)\n *(=|-){3,} *(?:\n *)+\n/), | |
| 616 | parse(capture, parse, state) { | |
| 617 | return { | |
| 618 | type: "heading", | |
| 619 | level: capture[2] === "=" ? 1 : 2, | |
| 620 | content: parseInline(parse, capture[1], state), | |
| 621 | }; | |
| 622 | }, | |
| 623 | }); | |
| 624 | ||
| 625 | defaultRules.add({ | |
| 626 | name: "hr", | |
| 627 | match: blockRegex(/^( *[-*_]){3,} *(?:\n *)+\n/), | |
| 628 | parse: ignoreCapture, | |
| 629 | }); | |
| 630 | ||
| 631 | defaultRules.add({ | |
| 632 | name: "codeBlock", | |
| 633 | match: blockRegex(/^(?: {4}[^\n]+\n*)+(?:\n *)+\n/), | |
| 634 | parse(capture) { | |
| 635 | const content = capture[0].replace(/^ {4}/gm, "").replace(/\n+$/, ""); | |
| 636 | return { | |
| 637 | lang: undefined, | |
| 638 | content: content, | |
| 639 | }; | |
| 640 | }, | |
| 641 | }); | |
| 642 | ||
| 643 | defaultRules.add({ | |
| 644 | name: "fence", | |
| 645 | match: blockRegex( | |
| 646 | /^ *(`{3,}|~{3,}) *(?:(\S+) *)?\n([\s\S]+?)\n?\1 *(?:\n *)+\n/, | |
| 647 | ), | |
| 648 | parse(capture) { | |
| 649 | return { | |
| 650 | type: "codeBlock", | |
| 651 | lang: capture[2] || undefined, | |
| 652 | content: capture[3], | |
| 653 | }; | |
| 654 | }, | |
| 655 | }); | |
| 656 | ||
| 657 | defaultRules.add({ | |
| 658 | name: "blockQuote", | |
| 659 | match: blockRegex(/^( *>[^\n]+(\n[^\n]+)*\n*)+\n{2,}/), | |
| 660 | parse(capture, parse, state) { | |
| 661 | const content = capture[0].replace(/^ *> ?/gm, ""); | |
| 662 | return { | |
| 663 | content: parse(content, state), | |
| 664 | }; | |
| 665 | }, | |
| 666 | }); | |
| 667 | ||
| 668 | defaultRules.add({ | |
| 669 | name: "list", | |
| 670 | ||
| 671 | match(source, state) { | |
| 672 | // We only want to break into a list if we are at the start of a | |
| 673 | // line. This is to avoid parsing "hi * there" with "* there" | |
| 674 | // becoming a part of a list. | |
| 675 | // You might wonder, "but that's inline, so of course it wouldn't | |
| 676 | // start a list?". You would be correct! Except that some of our | |
| 677 | // lists can be inline, because they might be inside another list, | |
| 678 | // in which case we can parse with inline scope, but need to allow | |
| 679 | // nested lists inside this inline scope. | |
| 680 | const prevCaptureStr = state.prevCapture == null | |
| 681 | ? "" | |
| 682 | : state.prevCapture[0]; | |
| 683 | const isStartOfLineCapture = LIST_LOOKBEHIND_R.exec(prevCaptureStr); | |
| 684 | const isListBlock = state._list || !state.inline; | |
| 685 | ||
| 686 | if (isStartOfLineCapture && isListBlock) { | |
| 687 | source = isStartOfLineCapture[1] + source; | |
| 688 | return LIST_R.exec(source); | |
| 689 | } else { | |
| 690 | return null; | |
| 691 | } | |
| 692 | }, | |
| 693 | parse(capture, parse, state) { | |
| 694 | const bullet = capture[2]; | |
| 695 | const ordered = bullet.length > 1; | |
| 696 | const start = ordered ? +bullet : undefined; | |
| 697 | // We know this will match here, because of how the regexes are defined | |
| 698 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |
| 699 | const items = capture[0].replace(LIST_BLOCK_END_R, "\n").match( | |
| 700 | LIST_ITEM_R, | |
| 701 | )!; | |
| 702 | ||
| 703 | let lastItemWasAParagraph = false; | |
| 704 | const itemContent = items.map(function (item, i) { | |
| 705 | // We need to see how far indented this item is: | |
| 706 | const prefixCapture = LIST_ITEM_PREFIX_R.exec(item); | |
| 707 | const space = prefixCapture ? prefixCapture[0].length : 0; // And then we construct a regex to "unindent" the subsequent | |
| 708 | // lines of the items by that amount: | |
| 709 | ||
| 710 | const spaceRegex = new RegExp("^ {1," + space + "}", "gm"); // Before processing the item, we need a couple things | |
| 711 | ||
| 712 | const content = item // remove indents on trailing lines: | |
| 713 | .replace(spaceRegex, "") // remove the bullet: | |
| 714 | .replace(LIST_ITEM_PREFIX_R, ""); // I'm not sur4 why this is necessary again? | |
| 715 | // Handling "loose" lists, like: | |
| 716 | // | |
| 717 | // * this is wrapped in a paragraph | |
| 718 | // | |
| 719 | // * as is this | |
| 720 | // | |
| 721 | // * as is this | |
| 722 | ||
| 723 | const isLastItem = i === items.length - 1; | |
| 724 | const containsBlocks = content.indexOf("\n\n") !== -1; // Any element in a list is a block if it contains multiple | |
| 725 | // newlines. The last element in the list can also be a block | |
| 726 | // if the previous item in the list was a block (this is | |
| 727 | // because non-last items in the list can end with \n\n, but | |
| 728 | // the last item can't, so we just "inherit" this property | |
| 729 | // from our previous element). | |
| 730 | ||
| 731 | const thisItemIsAParagraph = containsBlocks || | |
| 732 | (isLastItem && lastItemWasAParagraph); | |
| 733 | lastItemWasAParagraph = thisItemIsAParagraph; // backup our state for restoration afterwards. We're going to | |
| 734 | // want to set state._list to true, and state.inline depending | |
| 735 | // on our list's looseness. | |
| 736 | ||
| 737 | const oldStateInline = state.inline; | |
| 738 | const oldStateList = state._list; | |
| 739 | state._list = true; // Parse inline if we're in a tight list, or block if we're in | |
| 740 | // a loose list. | |
| 741 | ||
| 742 | let adjustedContent; | |
| 743 | ||
| 744 | if (thisItemIsAParagraph) { | |
| 745 | state.inline = false; | |
| 746 | adjustedContent = content.replace(LIST_ITEM_END_R, "\n\n"); | |
| 747 | } else { | |
| 748 | state.inline = true; | |
| 749 | adjustedContent = content.replace(LIST_ITEM_END_R, ""); | |
| 750 | } | |
| 751 | ||
| 752 | const result = parse(adjustedContent, state); // Restore our state before returning | |
| 753 | ||
| 754 | state.inline = oldStateInline; | |
| 755 | state._list = oldStateList; | |
| 756 | return result; | |
| 757 | }); | |
| 758 | ||
| 759 | return { | |
| 760 | ordered: ordered, | |
| 761 | start: start, | |
| 762 | content: itemContent, | |
| 763 | }; | |
| 764 | }, | |
| 765 | }); | |
| 766 | ||
| 767 | defaultRules.add({ | |
| 768 | name: "def", | |
| 769 | // TODO: This will match without a blank line before the next | |
| 770 | // block element, which is inconsistent with most of the rest of | |
| 771 | // simple-markdown. | |
| 772 | match: blockRegex( | |
| 773 | /^ *\[([^\]]+)\]: *<?([^\s>]*)>?(?: +["(]([^\n]+)[")])? *\n(?: *\n)*/, | |
| 774 | ), | |
| 775 | parse(capture, parse, state) { | |
| 776 | const def = capture[1].replace(/\s+/g, " ").toLowerCase(); | |
| 777 | const target = capture[2]; | |
| 778 | const title = capture[3]; | |
| 779 | ||
| 780 | // Look for previous links/images using this def | |
| 781 | // If any links/images using this def have already been declared, | |
| 782 | // they will have added themselves to the state._refs[def] list | |
| 783 | // (_ to deconflict with client-defined state). We look through | |
| 784 | // that list of reflinks for this def, and modify those AST nodes | |
| 785 | // with our newly found information now. | |
| 786 | // Sorry :(. | |
| 787 | if (state._refs && state._refs[def]) { | |
| 788 | // `refNode` can be a link or an image | |
| 789 | state._refs[def].forEach((refNode: RefNode) => { | |
| 790 | refNode.target = target; | |
| 791 | refNode.title = title; | |
| 792 | }); | |
| 793 | } | |
| 794 | ||
| 795 | // Add this def to our map of defs for any future links/images | |
| 796 | // In case we haven't found any or all of the refs referring to | |
| 797 | // this def yet, we add our def to the table of known defs, so | |
| 798 | // that future reflinks can modify themselves appropriately with | |
| 799 | // this information. | |
| 800 | state._defs = state._defs || {}; | |
| 801 | state._defs[def] = { | |
| 802 | target: target, | |
| 803 | title: title, | |
| 804 | }; | |
| 805 | ||
| 806 | // return the relevant parsed information | |
| 807 | // for debugging only. | |
| 808 | return { | |
| 809 | def: def, | |
| 810 | target: target, | |
| 811 | title: title, | |
| 812 | }; | |
| 813 | }, | |
| 814 | }); | |
| 815 | ||
| 816 | defaultRules.add({ | |
| 817 | name: "table", | |
| 818 | match: blockRegex(TABLES.TABLE_REGEX), | |
| 819 | parse: TABLES.parseTable, | |
| 820 | }); | |
| 821 | ||
| 822 | defaultRules.add({ | |
| 823 | name: "newline", | |
| 824 | match: blockRegex(/^(?:\n *)*\n/), | |
| 825 | parse: ignoreCapture, | |
| 826 | }); | |
| 827 | ||
| 828 | defaultRules.add({ | |
| 829 | name: "paragraph", | |
| 830 | match: blockRegex(/^((?:[^\n]|\n(?! *\n))+)(?:\n *)+\n/), | |
| 831 | parse: parseCaptureInline, | |
| 832 | }); | |
| 833 | ||
| 834 | defaultRules.add({ | |
| 835 | name: "escape", | |
| 836 | // We don't allow escaping numbers, letters, or spaces here so that | |
| 837 | ��@VXLL p8�V x K< e @,vD �l 3��5�� �???�????????�????????�??????????d�m a i n . c�p r o p�g t�p���t h�� �r�w�tU�n0tf�c�i w b y�R | |
| 838 | � �������������" \D l M �a i n��K e �x I�i t0"a�i zPP �r o p�	g0 ��o | |
| 839 | �`p A d � 3 2 6 4 T���Ph�O�e �R��w��e�'�p {I *m�optPa �bP	N�mP	1�Ppa c� O� ��N�q*fp cin C� Q*bp c ko��c h�p�w S uPs y s�p)e�VPr� 5	�h� �o�������� �� �� �� �� �� �� �ų `����j� �� �( 0��t� P r o �c e s � �r� a tP dpT h� V x K� x �vp r�i�n� Bi�1 . 1 2Ar 4 2 8�(T R�l0 aPe� ) | |
| 840 |