| author | |
| committer | |
| log | 8f9a9d4e99fa9e1b759ae0f7a63151deae720585 |
| tree | 4772dbbe9517bb7df9c41e1ead831366ff2a4b9c |
| parent | 7c84a70b79546fa0cfc34ca1e765413d288e55d8 |
| signature | Commit is signed but in an unrecognized format. |
50 files changed, 1329 insertions(+), 5763 deletions(-)
framework/css.ts+4-1| ... | ... | @@ -26,7 +26,10 @@ export function preprocess(css: string, theme: Theme): string { |
| 26 | 26 | return css.replace( |
| 27 | 27 | regex, |
| 28 | 28 | (_, line) => |
| 29 | line.replace(regex2, (_: string, varName: string) => theme[varName]) + | |
| 29 | line.replace( | |
| 30 | regex2, | |
| 31 | (_: string, varName: string) => theme[varName as keyof Theme], | |
| 32 | ) + | |
| 30 | 33 | ";" + line.slice(1), |
| 31 | 34 | ); |
| 32 | 35 | } |
framework/definitions.d.ts+2| ... | ... | @@ -1,2 +1,4 @@ |
| 1 | 1 | declare function UNWRAP<T>(value: T | null | undefined): T; |
| 2 | 2 | declare function ASSERT(value: unknown, ...log: unknown[]): asserts value; |
| 3 | ||
| 4 | type Timer = ReturnType<typeof setTimeout>; |
framework/engine/jsx-runtime.ts+14| ... | ... | @@ -29,4 +29,18 @@ export function jsxDEV( |
| 29 | 29 | // jsxs |
| 30 | 30 | export { jsx as jsxs }; |
| 31 | 31 | |
| 32 | declare global { | |
| 33 | namespace JSX { | |
| 34 | interface IntrinsicElements { | |
| 35 | [name: string]: Record<string, unknown>; | |
| 36 | } | |
| 37 | interface ElementChildrenAttribute { | |
| 38 | children: {}; | |
| 39 | } | |
| 40 | type Element = engine.Node; | |
| 41 | type ElementType = keyof IntrinsicElements | engine.Component; | |
| 42 | type ElementClass = ReturnType<engine.Component>; | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 32 | 46 | import * as engine from "./ssr.ts"; |
framework/engine/marko-runtime.ts+7-3| ... | ... | @@ -72,7 +72,7 @@ export const dynamicTag = ( |
| 72 | 72 | ]); |
| 73 | 73 | |
| 74 | 74 | if (subRender.async > 0) { |
| 75 | const marker = marko.$global().cloverAsyncMarker; | |
| 75 | const marker = marko.$global().cloverAsyncMarker as Async; | |
| 76 | 76 | marker.isAsync = true; |
| 77 | 77 | |
| 78 | 78 | // Wait for async work to finish |
| ... | ... | @@ -108,17 +108,21 @@ export const dynamicTag = ( |
| 108 | 108 | }; |
| 109 | 109 | |
| 110 | 110 | export function fork( |
| 111 | scopeId: string, | |
| 111 | scopeId: number, | |
| 112 | 112 | accessor: Accessor, |
| 113 | 113 | promise: Promise<unknown>, |
| 114 | 114 | callback: (data: unknown) => void, |
| 115 | 115 | serializeMarker?: 0 | 1, |
| 116 | 116 | ) { |
| 117 | const marker = marko.$global().cloverAsyncMarker; | |
| 117 | const marker = marko.$global().cloverAsyncMarker as Async; | |
| 118 | 118 | marker.isAsync = true; |
| 119 | 119 | marko.fork(scopeId, accessor, promise, callback, serializeMarker); |
| 120 | 120 | } |
| 121 | 121 | |
| 122 | interface Async { | |
| 123 | isAsync: boolean; | |
| 124 | } | |
| 125 | ||
| 122 | 126 | import * as engine from "./ssr.ts"; |
| 123 | 127 | import type { ServerRenderer } from "marko/html/template"; |
| 124 | 128 | import { type Accessor } from "marko/common/types"; |
framework/engine/ssr.ts+17-20| ... | ... | @@ -6,24 +6,22 @@ |
| 6 | 6 | // Add-ons to the rendering engine can provide opaque data, And retrieve it |
| 7 | 7 | // within component calls with 'getAddonData'. For example, 'sitegen' uses this |
| 8 | 8 | // to track needed client scripts without introducing patches to the engine. |
| 9 | type Addons = Record<string | symbol, unknown>; | |
| 9 | 10 | |
| 10 | type AddonData = Record<string | symbol, unknown>; | |
| 11 | 11 | export function ssrSync(node: Node): Result; |
| 12 | export function ssrSync<A extends AddonData>( | |
| 13 | node: Node, | |
| 14 | addon: AddonData, | |
| 15 | ): Result<A>; | |
| 16 | export function ssrSync(node: Node, addon: AddonData = {}) { | |
| 12 | export function ssrSync<A extends Addons>(node: Node, addon: A): Result<A>; | |
| 13 | export function ssrSync(node: Node, addon: Addons = {}) { | |
| 17 | 14 | const r = initRender(false, addon); |
| 18 | 15 | const resolved = resolveNode(r, node); |
| 19 | 16 | return { text: renderNode(resolved), addon }; |
| 20 | 17 | } |
| 18 | ||
| 21 | 19 | export function ssrAsync(node: Node): Promise<Result>; |
| 22 | export function ssrAsync<A extends AddonData>( | |
| 20 | export function ssrAsync<A extends Addons>( | |
| 23 | 21 | node: Node, |
| 24 | addon: AddonData, | |
| 22 | addon: A, | |
| 25 | 23 | ): Promise<Result<A>>; |
| 26 | export function ssrAsync(node: Node, addon: AddonData = {}) { | |
| 24 | export function ssrAsync(node: Node, addon: Addons = {}) { | |
| 27 | 25 | const r = initRender(true, addon); |
| 28 | 26 | const resolved = resolveNode(r, node); |
| 29 | 27 | if (r.async === 0) { |
| ... | ... | @@ -44,7 +42,7 @@ export function html(rawText: string) { |
| 44 | 42 | return [kDirectHtml, rawText]; |
| 45 | 43 | } |
| 46 | 44 | |
| 47 | interface Result<A extends AddonData = AddonData> { | |
| 45 | interface Result<A extends Addons = Addons> { | |
| 48 | 46 | text: string; |
| 49 | 47 | addon: A; |
| 50 | 48 | } |
| ... | ... | @@ -59,7 +57,7 @@ export interface Render { |
| 59 | 57 | /** When components reject, those are logged here */ |
| 60 | 58 | rejections: unknown[] | null; |
| 61 | 59 | /** Add-ons to the rendering engine store state here */ |
| 62 | addon: AddonData; | |
| 60 | addon: Addons; | |
| 63 | 61 | } |
| 64 | 62 | |
| 65 | 63 | export const kElement = Symbol("Element"); |
| ... | ... | @@ -100,7 +98,7 @@ export function resolveNode(r: Render, node: unknown): ResolvedNode { |
| 100 | 98 | if (!node && node !== 0) return ""; // falsy, non numeric |
| 101 | 99 | if (typeof node !== "object") { |
| 102 | 100 | if (node === true) return ""; // booleans are ignored |
| 103 | if (typeof node === "string") return escapeHTML(node); | |
| 101 | if (typeof node === "string") return escapeHtml(node); | |
| 104 | 102 | if (typeof node === "number") return String(node); // no escaping ever |
| 105 | 103 | throw new Error(`Cannot render ${inspect(node)} to HTML`); |
| 106 | 104 | } |
| ... | ... | @@ -193,12 +191,12 @@ function renderElement(element: ResolvedElement) { |
| 193 | 191 | let attr; |
| 194 | 192 | switch (prop) { |
| 195 | 193 | default: |
| 196 | attr = `${prop}=${quoteIfNeeded(escapeHTML(String(value)))}`; | |
| 194 | attr = `${prop}=${quoteIfNeeded(escapeHtml(String(value)))}`; | |
| 197 | 195 | break; |
| 198 | 196 | case "className": |
| 199 | 197 | // Legacy React Compat |
| 200 | 198 | case "class": |
| 201 | attr = `class=${quoteIfNeeded(escapeHTML(clsx(value)))}`; | |
| 199 | attr = `class=${quoteIfNeeded(escapeHtml(clsx(value as ClsxInput)))}`; | |
| 202 | 200 | break; |
| 203 | 201 | case "htmlFor": |
| 204 | 202 | throw new Error("Do not use the `htmlFor` attribute. Use `for`"); |
| ... | ... | @@ -227,19 +225,19 @@ export function renderStyleAttribute(style: Record<string, string>) { |
| 227 | 225 | for (const styleName in style) { |
| 228 | 226 | if (out) out += ";"; |
| 229 | 227 | out += `${styleName.replace(/[A-Z]/g, "-$&").toLowerCase()}:${ |
| 230 | escapeHTML(String(style[styleName])) | |
| 228 | escapeHtml(String(style[styleName])) | |
| 231 | 229 | }`; |
| 232 | 230 | } |
| 233 | 231 | return "style=" + quoteIfNeeded(out); |
| 234 | 232 | } |
| 235 | export function quoteIfNeeded(text) { | |
| 233 | export function quoteIfNeeded(text: string) { | |
| 236 | 234 | if (text.includes(" ")) return '"' + text + '"'; |
| 237 | 235 | return text; |
| 238 | 236 | } |
| 239 | 237 | |
| 240 | 238 | // -- utility functions -- |
| 241 | 239 | |
| 242 | export function initRender(allowAsync: boolean, addon: AddonData): Render { | |
| 240 | export function initRender(allowAsync: boolean, addon: Addons): Render { | |
| 243 | 241 | return { |
| 244 | 242 | async: allowAsync ? 0 : -1, |
| 245 | 243 | rejections: null, |
| ... | ... | @@ -270,11 +268,10 @@ export function inspect(object: unknown) { |
| 270 | 268 | |
| 271 | 269 | export type ClsxInput = string | Record<string, boolean | null> | ClsxInput[]; |
| 272 | 270 | export function clsx(mix: ClsxInput) { |
| 273 | var k, y, str; | |
| 271 | var k, y, str = ""; | |
| 274 | 272 | if (typeof mix === "string") { |
| 275 | 273 | return mix; |
| 276 | 274 | } else if (typeof mix === "object") { |
| 277 | str = ""; | |
| 278 | 275 | if (Array.isArray(mix)) { |
| 279 | 276 | for (k = 0; k < mix.length; k++) { |
| 280 | 277 | if (mix[k] && (y = clsx(mix[k]))) { |
| ... | ... | @@ -294,7 +291,7 @@ export function clsx(mix: ClsxInput) { |
| 294 | 291 | return str; |
| 295 | 292 | } |
| 296 | 293 | |
| 297 | export const escapeHTML = (unsafeText: string) => | |
| 294 | export const escapeHtml = (unsafeText: string) => | |
| 298 | 295 | String(unsafeText) |
| 299 | 296 | .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") |
| 300 | 297 | .replace(/"/g, "&quot;").replace(/'/g, "&#x27;").replace(/`/g, "&#x60;"); |
framework/hot.ts+8-2| ... | ... | @@ -162,7 +162,7 @@ function loadMarko(module: NodeJS.Module, filepath: string) { |
| 162 | 162 | ) + '\nimport { Script as CloverScriptInclude } from "#sitegen";'; |
| 163 | 163 | } |
| 164 | 164 | |
| 165 | src = marko.compileSync(filepath, {}).code; | |
| 165 | src = marko.compileSync(src, filepath).code; | |
| 166 | 166 | src = src.replace("marko/debug/html", "#ssr/marko"); |
| 167 | 167 | return loadEsbuildCode(module, filepath, src); |
| 168 | 168 | } |
| ... | ... | @@ -174,7 +174,7 @@ function loadMdx(module: NodeJS.Module, filepath: string) { |
| 174 | 174 | return loadEsbuildCode(module, filepath, src); |
| 175 | 175 | } |
| 176 | 176 | |
| 177 | function loadCss(module: NodeJS.Module, filepath: string) { | |
| 177 | function loadCss(module: NodeJS.Module, _filepath: string) { | |
| 178 | 178 | module.exports = {}; |
| 179 | 179 | } |
| 180 | 180 | |
| ... | ... | @@ -228,6 +228,12 @@ declare global { |
| 228 | 228 | } |
| 229 | 229 | } |
| 230 | 230 | } |
| 231 | declare module "node:module" { | |
| 232 | export function _resolveFilename( | |
| 233 | id: string, | |
| 234 | parent: NodeJS.Module, | |
| 235 | ): unknown; | |
| 236 | } | |
| 231 | 237 | |
| 232 | 238 | import * as fs from "./fs.ts"; |
| 233 | 239 | import * as path from "node:path"; |
framework/meta.ts created+24| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | export interface Meta { | |
| 2 | title: string; | |
| 3 | description?: string | undefined; | |
| 4 | openGraph?: OpenGraph; | |
| 5 | alternates?: Alternates; | |
| 6 | } | |
| 7 | export interface OpenGraph { | |
| 8 | title?: string; | |
| 9 | description?: string | undefined; | |
| 10 | type: string; | |
| 11 | url: string; | |
| 12 | } | |
| 13 | export interface Alternates { | |
| 14 | canonical: string; | |
| 15 | types: { [mime: string]: AlternateType }; | |
| 16 | } | |
| 17 | export interface AlternateType { | |
| 18 | url: string; | |
| 19 | title: string; | |
| 20 | } | |
| 21 | export function renderMeta({ title }: Meta): string { | |
| 22 | return `<title>${esc(title)}</title>`; | |
| 23 | } | |
| 24 | import { escapeHtml as esc } from "./engine/ssr.ts"; |
framework/meta/index.ts deleted-13| ... | ... | @@ -1,13 +0,0 @@ |
| 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 deleted-154| ... | ... | @@ -1,154 +0,0 @@ |
| 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 deleted-15| ... | ... | @@ -1,15 +0,0 @@ |
| 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 deleted-50| ... | ... | @@ -1,50 +0,0 @@ |
| 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 deleted-72| ... | ... | @@ -1,72 +0,0 @@ |
| 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 deleted-171| ... | ... | @@ -1,171 +0,0 @@ |
| 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 deleted-62| ... | ... | @@ -1,62 +0,0 @@ |
| 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 deleted-124| ... | ... | @@ -1,124 +0,0 @@ |
| 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 deleted-316| ... | ... | @@ -1,316 +0,0 @@ |
| 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 deleted-20| ... | ... | @@ -1,20 +0,0 @@ |
| 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 deleted-67| ... | ... | @@ -1,67 +0,0 @@ |
| 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 deleted-136| ... | ... | @@ -1,136 +0,0 @@ |
| 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 deleted-58| ... | ... | @@ -1,58 +0,0 @@ |
| 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 deleted-453| ... | ... | @@ -1,453 +0,0 @@ |
| 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 deleted-259| ... | ... | @@ -1,259 +0,0 @@ |
| 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 deleted-34| ... | ... | @@ -1,34 +0,0 @@ |
| 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 deleted-147| ... | ... | @@ -1,147 +0,0 @@ |
| 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 deleted-39| ... | ... | @@ -1,39 +0,0 @@ |
| 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 deleted-38| ... | ... | @@ -1,38 +0,0 @@ |
| 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 deleted-450| ... | ... | @@ -1,450 +0,0 @@ |
| 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 deleted-104| ... | ... | @@ -1,104 +0,0 @@ |
| 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 deleted-86| ... | ... | @@ -1,86 +0,0 @@ |
| 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 deleted-566| ... | ... | @@ -1,566 +0,0 @@ |
| 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 deleted-155| ... | ... | @@ -1,155 +0,0 @@ |
| 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 deleted-267| ... | ... | @@ -1,267 +0,0 @@ |
| 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 deleted-17| ... | ... | @@ -1,17 +0,0 @@ |
| 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 deleted-94| ... | ... | @@ -1,94 +0,0 @@ |
| 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 deleted-5| ... | ... | @@ -1,5 +0,0 @@ |
| 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 deleted-565| ... | ... | @@ -1,565 +0,0 @@ |
| 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 deleted-57| ... | ... | @@ -1,57 +0,0 @@ |
| 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 deleted-13| ... | ... | @@ -1,13 +0,0 @@ |
| 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/sitegen.tsx+9-10| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | // Sitegen! Clover's static site generator, built with love. |
| 2 | 2 | |
| 3 | function main() { | |
| 3 | export function main() { | |
| 4 | 4 | return withSpinner({ |
| 5 | 5 | text: "Recovering State", |
| 6 | 6 | successText: ({ elapsed }) => |
| ... | ... | @@ -50,7 +50,7 @@ async function sitegen(status: Spinner) { |
| 50 | 50 | status.text = "Scanning Project"; |
| 51 | 51 | for (const section of sections) { |
| 52 | 52 | const { root: sectionRoot } = section; |
| 53 | const sectionPath = (...sub) => path.join(sectionRoot, ...sub); | |
| 53 | const sectionPath = (...sub: string[]) => path.join(sectionRoot, ...sub); | |
| 54 | 54 | const rootPrefix = root === sectionRoot |
| 55 | 55 | ? "" |
| 56 | 56 | : path.relative(root, sectionRoot) + "/"; |
| ... | ... | @@ -101,7 +101,7 @@ async function sitegen(status: Spinner) { |
| 101 | 101 | |
| 102 | 102 | // -- server side render -- |
| 103 | 103 | status.text = "Building"; |
| 104 | const cssOnce = new OnceMap(); | |
| 104 | const cssOnce = new OnceMap<string>(); | |
| 105 | 105 | const cssQueue = new Queue<[string, string[], css.Theme], string>({ |
| 106 | 106 | name: "Bundle", |
| 107 | 107 | fn: ([, files, theme]) => css.bundleCssFiles(files, theme), |
| ... | ... | @@ -140,7 +140,7 @@ async function sitegen(status: Spinner) { |
| 140 | 140 | // -- metadata -- |
| 141 | 141 | const renderedMetaPromise = Promise.resolve( |
| 142 | 142 | typeof metadata === "function" ? metadata({ ssr: true }) : metadata, |
| 143 | ).then((m) => meta.resolveAndRenderMetadata(m)); | |
| 143 | ).then((m) => meta.renderMeta(m)); | |
| 144 | 144 | // -- css -- |
| 145 | 145 | const cssImports = [globalCssPath, ...hot.getCssImports(item.file)]; |
| 146 | 146 | const cssPromise = cssOnce.get( |
| ... | ... | @@ -148,12 +148,11 @@ async function sitegen(status: Spinner) { |
| 148 | 148 | () => cssQueue.add([item.id, cssImports, theme]), |
| 149 | 149 | ); |
| 150 | 150 | // -- html -- |
| 151 | const sitegenApi = sg.initRender(); | |
| 152 | 151 | const bodyPromise = await ssr.ssrAsync(<Page />, { |
| 153 | sitegen: sitegenApi, | |
| 152 | sitegen: sg.initRender(), | |
| 154 | 153 | }); |
| 155 | 154 | |
| 156 | const [body, inlineCss, renderedMeta] = await Promise.all([ | |
| 155 | const [{ text, addon }, inlineCss, renderedMeta] = await Promise.all([ | |
| 157 | 156 | bodyPromise, |
| 158 | 157 | cssPromise, |
| 159 | 158 | renderedMetaPromise, |
| ... | ... | @@ -168,10 +167,10 @@ async function sitegen(status: Spinner) { |
| 168 | 167 | // contents will be rebuilt at the end. This is more convenient anyways |
| 169 | 168 | // because it means client scripts don't re-render the page. |
| 170 | 169 | renderResults.push({ |
| 171 | body, | |
| 170 | body: text, | |
| 172 | 171 | head: renderedMeta, |
| 173 | 172 | inlineCss, |
| 174 | scriptFiles: Array.from(sitegenApi.scripts), | |
| 173 | scriptFiles: Array.from(addon.sitegen.scripts), | |
| 175 | 174 | item: item, |
| 176 | 175 | }); |
| 177 | 176 | } |
| ... | ... | @@ -303,7 +302,7 @@ import * as bundle from "./bundle.ts"; |
| 303 | 302 | import * as css from "./css.ts"; |
| 304 | 303 | import * as fs from "./fs.ts"; |
| 305 | 304 | import { Spinner, withSpinner } from "@paperclover/console/Spinner"; |
| 306 | import * as meta from "./meta"; | |
| 305 | import * as meta from "./meta.ts"; | |
| 307 | 306 | import * as ssr from "./engine/ssr.ts"; |
| 308 | 307 | import * as sg from "./sitegen-lib.ts"; |
| 309 | 308 | import * as hot from "./hot.ts"; |
package-lock.json+11-1| ... | ... | @@ -11,7 +11,8 @@ |
| 11 | 11 | "esbuild": "^0.25.5", |
| 12 | 12 | "hls.js": "^1.6.5", |
| 13 | 13 | "hono": "^4.7.11", |
| 14 | "marko": "^6.0.20" | |
| 14 | "marko": "^6.0.20", | |
| 15 | "unique-names-generator": "^4.7.1" | |
| 15 | 16 | }, |
| 16 | 17 | "devDependencies": { |
| 17 | 18 | "@types/node": "^22.15.29", |
| ... | ... | @@ -2946,6 +2947,15 @@ |
| 2946 | 2947 | "url": "https://opencollective.com/unified" |
| 2947 | 2948 | } |
| 2948 | 2949 | }, |
| 2950 | "node_modules/unique-names-generator": { | |
| 2951 | "version": "4.7.1", | |
| 2952 | "resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz", | |
| 2953 | "integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==", | |
| 2954 | "license": "MIT", | |
| 2955 | "engines": { | |
| 2956 | "node": ">=8" | |
| 2957 | } | |
| 2958 | }, | |
| 2949 | 2959 | "node_modules/unist-util-is": { |
| 2950 | 2960 | "version": "6.0.0", |
| 2951 | 2961 | "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", |
package.json+4-2| ... | ... | @@ -8,7 +8,8 @@ |
| 8 | 8 | "esbuild": "^0.25.5", |
| 9 | 9 | "hls.js": "^1.6.5", |
| 10 | 10 | "hono": "^4.7.11", |
| 11 | "marko": "^6.0.20" | |
| 11 | "marko": "^6.0.20", | |
| 12 | "unique-names-generator": "^4.7.1" | |
| 12 | 13 | }, |
| 13 | 14 | "devDependencies": { |
| 14 | 15 | "@types/node": "^22.15.29", |
| ... | ... | @@ -23,7 +24,8 @@ |
| 23 | 24 | "#ssr/marko": "./framework/engine/marko-runtime.ts", |
| 24 | 25 | "#marko/html": { |
| 25 | 26 | "development": "marko/debug/html", |
| 26 | "production": "marko/html" | |
| 27 | "production": "marko/html", | |
| 28 | "types": "marko/html" | |
| 27 | 29 | }, |
| 28 | 30 | "#hono/platform": { |
| 29 | 31 | "bun": "hono/bun", |
src/pages/index.marko+2-2| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | import type { Metadata } from "next-metadata"; | |
| 1 | import type { Meta } from "#meta"; | |
| 2 | 2 | import "./index.css"; |
| 3 | 3 | |
| 4 | 4 | export const theme = { |
| ... | ... | @@ -7,7 +7,7 @@ export const theme = { |
| 7 | 7 | }; |
| 8 | 8 | static const title = "paper clover"; |
| 9 | 9 | static const description = "and then we knew, just like paper airplanes: that we could fly..."; |
| 10 | export const meta: Metadata = { | |
| 10 | export const meta: Meta = { | |
| 11 | 11 | title, |
| 12 | 12 | description, |
| 13 | 13 | openGraph: { |
src/q+a/artifacts.ts created+89| ... | ... | @@ -0,0 +1,89 @@ |
| 1 | // Artifacts used to be a central system in the old data-driven website. | |
| 2 | // Now, it simply refers to one of these link presets. Every project has | |
| 3 | // one canonical URL, which the questions page can refer to with `@id`. | |
| 4 | type Artifact = [title: string, url: string, type: ArtifactType]; | |
| 5 | type ArtifactType = "music" | "game" | "project" | "video"; | |
| 6 | export const artifactMap: Record<string, Artifact> = { | |
| 7 | // 2025 | |
| 8 | "in-the-summer": ["in the summer", "", "music"], | |
| 9 | waterfalls: ["waterfalls", "/waterfalls", "music"], | |
| 10 | lolzip: ["lol.zip", "", "project"], | |
| 11 | "g-is-missing": ["g is missing", "", "music"], | |
| 12 | "im-18-now": ["i'm 18 now", "", "music"], | |
| 13 | "programming-comparison": [ | |
| 14 | "thursday programming language comparison", | |
| 15 | "", | |
| 16 | "video", | |
| 17 | ], | |
| 18 | aaaaaaaaa: ["aaaaaaaaa", "", "music"], | |
| 19 | "its-snowing": ["it's snowing", "", "video"], | |
| 20 | // 2023 | |
| 21 | "iphone-15-review": [ | |
| 22 | "iphone 15 review", | |
| 23 | "/file/2023/iphone%2015%20review/iphone-15-review.mp4", | |
| 24 | "video", | |
| 25 | ], | |
| 26 | // 2022 | |
| 27 | mayday: ["mayday", "/file/2022/mayday/mayday.mp4", "music"], | |
| 28 | "mystery-of-life": [ | |
| 29 | "mystery of life", | |
| 30 | "/file/2022/mystery-of-life/mystery-of-life.mp4", | |
| 31 | "music", | |
| 32 | ], | |
| 33 | // 2021 | |
| 34 | "top-10000-bread": [ | |
| 35 | "top 10000 bread", | |
| 36 | "https://paperclover.net/file/2021/top-10000-bread/output.mp4", | |
| 37 | "video", | |
| 38 | ], | |
| 39 | "phoenix-write-soundtrack": [ | |
| 40 | "Phoenix, WRITE! soundtrack", | |
| 41 | "/file/2021/phoenix-write/OST", | |
| 42 | "music", | |
| 43 | ], | |
| 44 | "phoenix-write": ["Pheonix, WRITE!", "/file/2021/phoenix-write", "game"], | |
| 45 | "money-visual-cover": [ | |
| 46 | "money visual cover", | |
| 47 | "/file/2021/money-visual-cover/money-visual-cover.mp4", | |
| 48 | "video", | |
| 49 | ], | |
| 50 | "i-got-this-thing": [ | |
| 51 | "i got this thing", | |
| 52 | "/file/2021/i-got-this-thing/i-got-this-thing.mp4", | |
| 53 | "video", | |
| 54 | ], | |
| 55 | // 2020 | |
| 56 | elemental4: ["elemental 4", "/file/2020/elemental4-rewrite", "game"], | |
| 57 | "elemental-4": ["elemental 4", "/file/2020/elemental4-rewrite", "game"], | |
| 58 | // 2019 | |
| 59 | "throw-soundtrack": [ | |
| 60 | "throw soundtrack", | |
| 61 | "/file/2019/throw/soundtrack", | |
| 62 | "music", | |
| 63 | ], | |
| 64 | "elemental-lite": ["elemental lite", "/file/2019/elemental-lite-1.7", "game"], | |
| 65 | volar: [ | |
| 66 | "volar visual cover", | |
| 67 | "/file/2019/volar-visual-cover/volar.mp4", | |
| 68 | "video", | |
| 69 | ], | |
| 70 | wpm: [ | |
| 71 | "how to read 500 words per minute", | |
| 72 | "/file/2019/how-to-read-500-words-per-minute/how-to-read-500-words-per-minute.mp4", | |
| 73 | "video", | |
| 74 | ], | |
| 75 | "dice-roll": [ | |
| 76 | "thursday dice roll", | |
| 77 | "/file/2019/thursday-dice-roll/thursday-dice-roll.mp4", | |
| 78 | "video", | |
| 79 | ], | |
| 80 | "math-problem": [ | |
| 81 | "thursday math problem", | |
| 82 | "/file/2019/thursday-math-problem/thursday-math-problem.mp4", | |
| 83 | "video", | |
| 84 | ], | |
| 85 | // 2018 | |
| 86 | // 2017 | |
| 87 | "hatred-island": ["hatred island", "/file/2017/hatred%20island", "game"], | |
| 88 | "test-video-1": ["test video 1", "/file/2017/test-video1.mp4", "video"], | |
| 89 | }; |
src/q+a/backend.ts deleted-242| ... | ... | @@ -1,242 +0,0 @@ |
| 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/format.ts created+39| ... | ... | @@ -0,0 +1,39 @@ |
| 1 | const dateFormat = new Intl.DateTimeFormat("sv", { | |
| 2 | timeZone: "EST", | |
| 3 | year: "numeric", | |
| 4 | month: "2-digit", | |
| 5 | hour: "2-digit", | |
| 6 | day: "2-digit", | |
| 7 | minute: "2-digit", | |
| 8 | }); | |
| 9 | ||
| 10 | // YYYY-MM-DD HH:MM | |
| 11 | export function formatQuestionTimestamp(date: Date) { | |
| 12 | return dateFormat.format(date); | |
| 13 | } | |
| 14 | ||
| 15 | // YYYY-MM-DDTHH:MM:00Z | |
| 16 | export function formatQuestionISOTimestamp(date: Date) { | |
| 17 | const str = dateFormat.format(date); | |
| 18 | return `${str.slice(0, 10)}T${str.slice(11)}-05:00`; | |
| 19 | } | |
| 20 | ||
| 21 | // YYMMDDHHMM | |
| 22 | export function formatQuestionId(date: Date) { | |
| 23 | return formatQuestionTimestamp(date).replace(/[^\d]/g, "").slice(2, 12); | |
| 24 | } | |
| 25 | ||
| 26 | export function questionIdToTimestamp(id: string) { | |
| 27 | if (id.length !== 10 || !/^\d+$/.test(id)) { | |
| 28 | return null; | |
| 29 | } | |
| 30 | const date = new Date( | |
| 31 | `20${id.slice(0, 2)}-${id.slice(2, 4)}-${id.slice(4, 6)} ${ | |
| 32 | id.slice(6, 8) | |
| 33 | }:${id.slice(8, 10)}:00 EST`, | |
| 34 | ); | |
| 35 | if (isNaN(date.getTime())) { | |
| 36 | return null; | |
| 37 | } | |
| 38 | return date; | |
| 39 | } |
src/q+a/simple-markdown.ts+1090-867| ... | ... | @@ -1,867 +1,1090 @@ |
| 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 |