├── .gitignore ├── src ├── index.ts ├── types │ ├── limits.ts │ ├── posts.ts │ ├── access-result.ts │ ├── tagged.ts │ ├── attachments.ts │ ├── asks.ts │ ├── ids.ts │ ├── username-verifier.ts │ ├── projects.ts │ ├── wire-models.ts │ └── post-blocks.ts ├── components │ ├── custom-emoji.tsx │ ├── mention.tsx │ └── info-box.tsx └── lib │ ├── shared-types.ts │ ├── mention-parsing.ts │ ├── emoji.ts │ ├── other-rendering.ts │ ├── unified-processors.ts │ ├── sanitize.tsx │ └── post-rendering.tsx ├── rollup.config.ts ├── tsconfig.json ├── LICENSE ├── readme.md ├── package.json └── pnpm-lock.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .DS_Store -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/post-rendering"; 2 | export * from "./lib/other-rendering"; 3 | -------------------------------------------------------------------------------- /src/types/limits.ts: -------------------------------------------------------------------------------- 1 | // length limit for usernames and project handles 2 | export const USERNAME_HANDLE_LIMIT = 200; 3 | 4 | // length limit for asks 5 | export const ASK_LENGTH_LIMIT = 1120; 6 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | 3 | export default { 4 | input: "src/index.ts", 5 | output: { 6 | dir: "dist", 7 | format: "es", 8 | sourcemap: true, 9 | }, 10 | plugins: [typescript()], 11 | }; 12 | -------------------------------------------------------------------------------- /src/types/posts.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export enum PostState { 4 | Unpublished = 0, 5 | Published, 6 | Deleted, 7 | } 8 | 9 | export const PostStateEnum = z.enum(PostState); 10 | export type PostStateEnum = z.infer; 11 | -------------------------------------------------------------------------------- /src/types/access-result.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export enum AccessResult { 4 | Allowed = "allowed", 5 | NotAllowed = "not-allowed", 6 | LogInFirst = "log-in-first", 7 | Blocked = "blocked", 8 | } 9 | 10 | export const AccessResultEnum = z.enum(AccessResult); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "jsx": "react-jsx", 5 | "esModuleInterop": true, 6 | "target": "esnext", 7 | "moduleResolution": "node", 8 | "noEmit": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["./src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /src/types/tagged.ts: -------------------------------------------------------------------------------- 1 | // based on https://github.com/colinhacks/zod/issues/678#issuecomment-962387521 2 | export type Tagged = T & { __tag: Tag }; 3 | export function refinement, T>() { 4 | return function (val: T): val is Type { 5 | return true; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/custom-emoji.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | 3 | export const CustomEmoji: FunctionComponent<{ 4 | name: string; 5 | url: string; 6 | }> = React.memo(({ name = "missing", url = "" }) => { 7 | return ( 8 | {`:${name}:`} 17 | ); 18 | }); 19 | CustomEmoji.displayName = "CustomEmoji"; 20 | -------------------------------------------------------------------------------- /src/components/mention.tsx: -------------------------------------------------------------------------------- 1 | import { ProjectHandle } from "../types/ids"; 2 | import React, { FunctionComponent } from "react"; 3 | 4 | export const Mention: FunctionComponent<{ handle: ProjectHandle }> = ({ 5 | handle, 6 | }) => { 7 | return ( 8 | 13 | @{handle} 14 | 15 | ); 16 | }; 17 | 18 | /** 19 | * Default props included because Mention is used outside of typescript and we 20 | * need an easy way to see when it's fucked instead of just crashing 21 | */ 22 | Mention.defaultProps = { 23 | handle: "ERROR" as ProjectHandle, 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/shared-types.ts: -------------------------------------------------------------------------------- 1 | export type RenderingContext = 2 | | "activitypub" 3 | | "ask" 4 | | "comment" 5 | | "email" 6 | | "post" 7 | | "profile" 8 | | "rss" 9 | | "artistAlley" 10 | | null; 11 | 12 | export type RenderingOptions = { 13 | /** the context in which this content is being rendered */ 14 | renderingContext: RenderingContext; 15 | /** 16 | * whether or not the posting user has cohost plus (currently determines 17 | * emoji rendering behavior) 18 | */ 19 | hasCohostPlus: boolean; 20 | externalLinksInNewTab: boolean; 21 | }; 22 | 23 | /** 24 | * line length limit for enabling GFM tables. workaround for a performance issue 25 | * with long profile descriptions. 26 | */ 27 | export const MAX_GFM_LINES = 256; 28 | -------------------------------------------------------------------------------- /src/types/attachments.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | 3 | export enum AttachmentState { 4 | Pending = 0, 5 | Finished, 6 | } 7 | 8 | export const AttachmentKind = z.enum(["audio", "image"]); 9 | export type AttachmentKind = z.infer; 10 | 11 | export const WireAudioAttachmentMetadata = z.object({ 12 | title: z.string().optional(), 13 | artist: z.string().optional(), 14 | }); 15 | export type WireAudioAttachmentMetadata = z.infer< 16 | typeof WireAudioAttachmentMetadata 17 | >; 18 | 19 | export const WireImageAttachmentMetadata = z.object({}); 20 | export type WireImageAttachmentMetadata = z.infer< 21 | typeof WireImageAttachmentMetadata 22 | >; 23 | 24 | export const WireAnyAttachmentMetadata = z.union([ 25 | WireAudioAttachmentMetadata, 26 | WireImageAttachmentMetadata, 27 | ]); 28 | export type WireAnyAttachmentMetadata = z.infer< 29 | typeof WireAnyAttachmentMetadata 30 | >; 31 | -------------------------------------------------------------------------------- /src/components/info-box.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, ReactElement, useContext } from "react"; 2 | import { z } from "zod"; 3 | 4 | export const InfoBoxLevel = z.enum([ 5 | "info", 6 | "warning", 7 | "done", 8 | "post-box-info", 9 | "post-box-warning", 10 | ]); 11 | export type InfoBoxLevel = z.infer; 12 | 13 | type InfoBoxProps = { 14 | level: InfoBoxLevel; 15 | }; 16 | 17 | export const InfoBox: FunctionComponent< 18 | React.PropsWithChildren 19 | > = ({ level, children }) => { 20 | let bgClasses: string; 21 | 22 | switch (level) { 23 | case "info": 24 | bgClasses = "co-info-box co-info"; 25 | break; 26 | case "warning": 27 | bgClasses = "co-info-box co-warning"; 28 | break; 29 | case "done": 30 | bgClasses = "co-info-box co-done"; 31 | break; 32 | case "post-box-info": 33 | bgClasses = "co-info-box co-post-info"; 34 | break; 35 | case "post-box-warning": 36 | bgClasses = "co-info-box co-post-warning"; 37 | break; 38 | } 39 | 40 | return
{children}
; 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 anti software software club LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/types/asks.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { AskId, ISODateString, ProjectHandle, ProjectId } from "./ids"; 3 | import { AvatarShape, ProjectFlag, ProjectPrivacyEnum } from "./projects"; 4 | 5 | export const AskState = z.enum(["pending", "responded", "deleted"]); 6 | export type AskState = z.infer; 7 | 8 | const FilteredProject = z.object({ 9 | projectId: ProjectId, 10 | handle: ProjectHandle, 11 | avatarURL: z.string().url(), 12 | avatarPreviewURL: z.string().url(), 13 | privacy: ProjectPrivacyEnum, 14 | flags: ProjectFlag.array(), 15 | avatarShape: AvatarShape, 16 | displayName: z.string(), 17 | }); 18 | 19 | export const WireAskModel = z.discriminatedUnion("anon", [ 20 | z.object({ 21 | anon: z.literal(true), 22 | loggedIn: z.boolean(), 23 | askingProject: z.undefined(), 24 | askId: AskId, 25 | content: z.string(), 26 | sentAt: ISODateString, 27 | }), 28 | z.object({ 29 | anon: z.literal(false), 30 | loggedIn: z.literal(true), 31 | askingProject: FilteredProject, 32 | askId: AskId, 33 | content: z.string(), 34 | sentAt: ISODateString, 35 | }), 36 | ]); 37 | export type WireAskModel = z.infer; 38 | -------------------------------------------------------------------------------- /src/types/ids.ts: -------------------------------------------------------------------------------- 1 | import { EXTANT_PAGE_LEGAL_REGEX } from "./username-verifier"; 2 | import { DateTime } from "luxon"; 3 | import z from "zod"; 4 | import { Tagged, refinement } from "./tagged"; 5 | 6 | const BigIntId = z 7 | .string() 8 | .or(z.number().transform((val) => val.toString())) 9 | .refine((val) => { 10 | try { 11 | // BigInt throws on non-integer strings 12 | const num = BigInt(val); 13 | return num > BigInt(0); 14 | } catch (e) { 15 | return false; 16 | } 17 | }); 18 | 19 | export const AttachmentId = z.uuid().brand<"AttachmentId">(); 20 | export type AttachmentId = z.infer; 21 | 22 | export const PostId = z.int().brand<"PostId">(); 23 | export type PostId = z.infer; 24 | 25 | export const ProjectId = z.int().brand<"ProjectId">(); 26 | export type ProjectId = z.infer; 27 | 28 | export const UserId = z.int().brand<"UserId">(); 29 | export type UserId = z.infer; 30 | 31 | export const ProjectHandle = z 32 | .string() 33 | // use the old legal regex to prevent tRPC errors with DNS-illegal usernames 34 | .regex(EXTANT_PAGE_LEGAL_REGEX) 35 | .brand<"ProjectHandle">(); 36 | export type ProjectHandle = z.infer; 37 | 38 | export const CommentId = z.uuid().brand<"CommentId">(); 39 | export type CommentId = z.infer; 40 | 41 | export const ISODateString = z.iso.datetime({ offset: true }); 42 | 43 | export const AskId = BigIntId.brand<"AskId">(); 44 | export type AskId = z.infer; 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # cohost renderer 2 | 3 | > No effort has been made to validate that this actually works outside of typechecking and bundling! 4 | 5 | bare-minimum version that builds. no package yet, no instructions yet, this is 6 | just the bare minimum extraction to make this build outside the cohost codebase. 7 | i strongly recommend against using this for anything serious. 8 | 9 | things that are missing that will probably not be added: 10 | 11 | - `InfoBox` is a bare-minimum implementation that doesn't use any of our styling 12 | because i didn't want to make tailwind a dependency here 13 | - custom emoji loading is not implemented as it was extremely dependent on the 14 | cohost build system. if you can populate the arrays then it should render, but 15 | doing that is left as an exercise to the reader. 16 | - probably other things i'm not thinking about 17 | 18 | things that are missing that will be added: 19 | 20 | - instructions 21 | - an example 22 | - an npm package 23 | 24 | things that are here that will probably be removed at some point: 25 | 26 | - extraneous IDs, types, etc. this was pulled straight out of the cohost 27 | codebase and while i removed the obviously unnecessary bits (things related to 28 | invites, for example) there might be some bonus bullshit in here. 29 | - ~~code related to AST caching. it's entirely unnecessary for non-production use.~~ it's gone now 30 | 31 | # license 32 | 33 | this release is licensed under the MIT license. the remaining cohost frontend 34 | codebase is source-available (if you know where to look) but not open-source, 35 | the backend remains private. we have no intention to open-source the remainder 36 | of cohost at this time. 37 | -------------------------------------------------------------------------------- /src/types/username-verifier.ts: -------------------------------------------------------------------------------- 1 | import { USERNAME_HANDLE_LIMIT } from "./limits"; 2 | 3 | export type UsernameLegalResult = 4 | | { 5 | legal: true; 6 | } 7 | | { 8 | legal: false; 9 | reason: string; 10 | }; 11 | 12 | export const LEGAL_REGEX_STRING = "^[a-zA-Z0-9][a-zA-Z0-9-]{2,}$"; 13 | export const LEGAL_REGEX = new RegExp(LEGAL_REGEX_STRING); 14 | // separate regular expression for existing pages; fixes tRPC bugs with pages that are invalid DNS names but already exist 15 | export const EXTANT_PAGE_LEGAL_REGEX = /^[a-zA-Z0-9-]{3,}/; 16 | const BANNED_USERNAMES = new Set([ 17 | "rc", 18 | "api", 19 | "www", 20 | "help", 21 | "admin", 22 | "support", 23 | "staff", 24 | "internal", 25 | "status", 26 | "mail", 27 | "mobile", 28 | "search", 29 | "static", 30 | ]); 31 | 32 | export function isHandleLegal(username: string): UsernameLegalResult { 33 | const downcasedUsername = username.toLowerCase(); 34 | 35 | // no namespace usernames 36 | if (BANNED_USERNAMES.has(downcasedUsername)) { 37 | return { 38 | legal: false, 39 | reason: `Username can not be "${username}"`, 40 | }; 41 | } 42 | 43 | // legal characters only 44 | if (!LEGAL_REGEX.test(username)) { 45 | return { 46 | legal: false, 47 | 48 | reason: 49 | "Usernames can only contain letters, numbers, and hyphens, and must be at least 3 characters.", 50 | }; 51 | } 52 | 53 | if (downcasedUsername.length > USERNAME_HANDLE_LIMIT) { 54 | return { 55 | legal: false, 56 | reason: 57 | "Your username cannot be longer than 200 characters, but nice try.", 58 | }; 59 | } 60 | 61 | return { legal: true }; 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cohost-renderer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "module": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "rollup -c --configPlugin @rollup/plugin-typescript" 10 | }, 11 | "keywords": [], 12 | "author": "anti software software club llc (https://antisoftware.club)", 13 | "contributors": [ 14 | "jae kaplan (https://jkap.io)", 15 | "colin bayer (https://gameboat.org)" 16 | ], 17 | "license": "MIT", 18 | "files": [ 19 | "dist" 20 | ], 21 | "devDependencies": { 22 | "@rollup/plugin-typescript": "^12.1.4", 23 | "@types/hast": "^2.3.4", 24 | "@types/html-to-text": "^9.0.4", 25 | "@types/lodash": "^4.17.20", 26 | "@types/luxon": "^3.7.1", 27 | "@types/mdast": "^3.0.10", 28 | "@types/react": "^18", 29 | "hast-util-sanitize": "^5.0.2", 30 | "html-to-text": "^9.0.5", 31 | "lodash": "^4.17.21", 32 | "rollup": "^4.52.4", 33 | "tslib": "^2.8.1", 34 | "typescript": "^5.9.3", 35 | "react": "^18" 36 | }, 37 | "peerDependencies": { 38 | "react": ">=16" 39 | }, 40 | "peerDependenciesMeta": { 41 | "react": { 42 | "optional": true 43 | } 44 | }, 45 | "dependencies": { 46 | "deepmerge": "^4.3.1", 47 | "luxon": "^3.7.2", 48 | "remark": "^15.0.1", 49 | "style-to-object": "^1.0.11", 50 | "zod": "^4.1.12", 51 | "hast-util-sanitize": "^4.0.0", 52 | "html-to-text": "^9.0.5", 53 | "lodash": "^4.17.21", 54 | "rehype-external-links": "^2.0.0", 55 | "rehype-raw": "^6.1.1", 56 | "rehype-react": "^7.1.1", 57 | "rehype-remark": "^9.1.2", 58 | "rehype-sanitize": "^5.0.1", 59 | "rehype-stringify": "^9.0.3", 60 | "remark-breaks": "^3.0.3", 61 | "remark-gfm": "^3.0.1", 62 | "remark-parse": "^10.0.1", 63 | "remark-rehype": "^10.1.0", 64 | "remark-stringify": "^10.0.2", 65 | "unified": "^10.1.2", 66 | "unist-builder": "^3.0.0", 67 | "unist-util-is": "^5.2.0", 68 | "unist-util-visit": "^4.1.0", 69 | "unist-util-visit-parents": "^5.1.3" 70 | } 71 | } -------------------------------------------------------------------------------- /src/types/projects.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { ISODateString, ProjectHandle, ProjectId } from "./ids"; 3 | 4 | export enum ProjectPrivacy { 5 | Public = "public", 6 | Private = "private", 7 | } 8 | export const ProjectPrivacyEnum = z.enum(ProjectPrivacy); 9 | export type ProjectPrivacyEnum = z.infer; 10 | 11 | // shout out https://stackoverflow.com/a/61129291 12 | export const ProjectFlag = z.enum([ 13 | "staff", 14 | "staffMember", 15 | "friendOfTheSite", 16 | "noTransparentAvatar", 17 | "suspended", 18 | "automated", // used for the bot badge 19 | "parody", // used for the "un-verified" badge 20 | ]); 21 | export type ProjectFlag = z.infer; 22 | 23 | // TODO: the DB also supports "all-ages", but this setting doesn't currently 24 | // work because of an NYI access checker upgrade. 25 | export const LoggedOutPostVisibility = z.enum(["public", "none"]); 26 | export type LoggedOutPostVisibility = z.infer; 27 | 28 | export function isProjectFlag( 29 | maybeProjectFlag: unknown 30 | ): maybeProjectFlag is ProjectFlag { 31 | return ProjectFlag.safeParse(maybeProjectFlag).success; 32 | } 33 | 34 | export const AvatarShape = z.enum([ 35 | "circle", 36 | "roundrect", 37 | "squircle", 38 | "capsule-big", 39 | "capsule-small", 40 | "egg", 41 | ]); 42 | export type AvatarShape = z.infer; 43 | 44 | export const WireProjectModel = z.object({ 45 | projectId: ProjectId, 46 | handle: ProjectHandle, 47 | displayName: z.string(), 48 | dek: z.string(), 49 | description: z.string(), 50 | avatarURL: z.string().url(), 51 | avatarPreviewURL: z.string().url(), 52 | headerURL: z.string().url().nullable(), 53 | headerPreviewURL: z.string().url().nullable(), 54 | privacy: ProjectPrivacyEnum, 55 | url: z.string().nullable(), 56 | pronouns: z.string().nullable(), 57 | flags: ProjectFlag.array(), 58 | avatarShape: AvatarShape, 59 | loggedOutPostVisibility: LoggedOutPostVisibility, 60 | frequentlyUsedTags: z.string().array(), 61 | askSettings: z.object({ 62 | enabled: z.boolean(), 63 | allowAnon: z.boolean(), 64 | requireLoggedInAnon: z.boolean(), 65 | }), 66 | deleteAfter: ISODateString.nullable(), 67 | isSelfProject: z.boolean().nullable(), 68 | }); 69 | export type WireProjectModel = z.infer; 70 | -------------------------------------------------------------------------------- /src/lib/mention-parsing.ts: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/twitter/twitter-text 2 | 3 | function regexSupplant( 4 | regex: RegExp | string, 5 | map: Record, 6 | flags = "" 7 | ) { 8 | if (typeof regex !== "string") { 9 | if (regex.global && flags.indexOf("g") < 0) { 10 | flags += "g"; 11 | } 12 | if (regex.ignoreCase && flags.indexOf("i") < 0) { 13 | flags += "i"; 14 | } 15 | if (regex.multiline && flags.indexOf("m") < 0) { 16 | flags += "m"; 17 | } 18 | 19 | regex = regex.source; 20 | } 21 | 22 | return new RegExp( 23 | regex.replace(/#\{(\w+)\}/g, function (match, name: string) { 24 | let newRegex = map[name] || ""; 25 | if (typeof newRegex !== "string") { 26 | newRegex = newRegex.source; 27 | } 28 | return newRegex; 29 | }), 30 | flags 31 | ); 32 | } 33 | 34 | const latinAccentChars = 35 | /\xC0-\xD6\xD8-\xF6\xF8-\xFF\u0100-\u024F\u0253\u0254\u0256\u0257\u0259\u025B\u0263\u0268\u026F\u0272\u0289\u028B\u02BB\u0300-\u036F\u1E00-\u1EFF/; 36 | const atSigns = /[@@]/; 37 | const validMentionPrecedingChars = 38 | /(?:^|[^a-zA-Z0-9_!#$%&*@@\\/]|(?:^|[^a-zA-Z0-9_+~.-\\/]))/; 39 | 40 | const validMention = regexSupplant( 41 | "(#{validMentionPrecedingChars})" + // $1: Preceding character 42 | "(#{atSigns})" + // $2: At mark 43 | "([a-zA-Z0-9-]{3,})", // $3: handle 44 | { validMentionPrecedingChars, atSigns }, 45 | "g" 46 | ); 47 | const endMentionMatch = regexSupplant( 48 | /^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/, 49 | { atSigns, latinAccentChars } 50 | ); 51 | 52 | type MentionToken = { 53 | handle: string; 54 | indices: [startPosition: number, endPosition: number]; 55 | }; 56 | export function extractMentions(text: string): MentionToken[] { 57 | if (!text.match(atSigns)) { 58 | return []; 59 | } 60 | 61 | const possibleNames: MentionToken[] = []; 62 | 63 | text.replace( 64 | validMention, 65 | function ( 66 | match, 67 | before: string, 68 | atSign: string, 69 | handle: string, 70 | offset: number, 71 | chunk: string 72 | ) { 73 | const after = chunk.slice(offset + match.length); 74 | 75 | if (!after.match(endMentionMatch)) { 76 | const startPosition = offset + before.length; 77 | const endPosition = startPosition + handle.length + 1; 78 | possibleNames.push({ 79 | handle, 80 | indices: [startPosition, endPosition], 81 | }); 82 | } 83 | 84 | return ""; 85 | } 86 | ); 87 | 88 | return possibleNames; 89 | } 90 | -------------------------------------------------------------------------------- /src/lib/emoji.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { SKIP } from "unist-util-visit"; 3 | import { processMatches } from "./unified-processors"; 4 | import type { Plugin, Compiler } from "unified"; 5 | import { Element, Text, type Root } from "hast"; 6 | 7 | const EMOJI_REGEX = /:[a-zA-Z\d-_]+:/gims; 8 | 9 | export type CustomEmoji = { 10 | id: string; 11 | name: string; 12 | keywords: string[]; 13 | skins: { src: string }[]; 14 | native?: undefined; 15 | // added by the library; we don't need to set it 16 | shortcodes?: string; 17 | }; 18 | type CustomEmojiCategory = { 19 | id: string; 20 | name: string; 21 | emojis: CustomEmoji[]; 22 | }; 23 | export type CustomEmojiSet = CustomEmojiCategory[]; 24 | 25 | type NativeEmoji = { 26 | id: string; 27 | keywords: string[]; 28 | name: string; 29 | native: string; 30 | shortcodes: string; 31 | unified: string; 32 | }; 33 | 34 | export type Emoji = NativeEmoji | CustomEmoji; 35 | 36 | export const customEmoji: CustomEmoji[] = []; 37 | 38 | export const cohostPlusCustomEmoji: CustomEmoji[] = []; 39 | 40 | export const indexableCustomEmoji = new Map( 41 | customEmoji.reduce<[string, CustomEmoji][]>((collector, emoji) => { 42 | return [...collector, [emoji.name, emoji]]; 43 | }, []) 44 | ); 45 | 46 | export const indexableCohostPlusCustomEmoji = new Map( 47 | cohostPlusCustomEmoji.reduce<[string, CustomEmoji][]>((collector, emoji) => { 48 | return [...collector, [emoji.name, emoji]]; 49 | }, []) 50 | ); 51 | 52 | type ParseOptions = { 53 | cohostPlus: boolean; 54 | }; 55 | 56 | export const parseEmoji = (options: ParseOptions) => { 57 | const compiler: Compiler = processMatches( 58 | EMOJI_REGEX, 59 | (matches, splits, node, index, parent) => { 60 | const els = splits.reduce>( 61 | (collector, curr, index) => { 62 | const currNode: Text = { 63 | type: "text", 64 | value: curr, 65 | }; 66 | 67 | const pending = [...collector, currNode]; 68 | 69 | if (index < matches.length) { 70 | const emojiName = matches[index].slice( 71 | 1, 72 | matches[index].length - 1 73 | ); 74 | let emoji = indexableCustomEmoji.get(emojiName); 75 | if (!emoji && options.cohostPlus) { 76 | emoji = indexableCohostPlusCustomEmoji.get(emojiName); 77 | } 78 | 79 | if (emoji) { 80 | pending.push({ 81 | type: "element", 82 | tagName: "CustomEmoji", 83 | properties: { 84 | name: emoji.name, 85 | url: emoji.skins[0].src, 86 | }, 87 | children: [], 88 | } as Element); 89 | } else { 90 | pending.push({ 91 | type: "text", 92 | value: matches[index], 93 | }); 94 | } 95 | } 96 | 97 | return pending; 98 | }, 99 | [] as Array 100 | ); 101 | parent.children.splice(index, 1, ...els); 102 | // skip over all the new elements we just created 103 | return [SKIP, index + els.length]; 104 | } 105 | ); 106 | return compiler; 107 | }; 108 | -------------------------------------------------------------------------------- /src/lib/other-rendering.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * For comments, page descriptions, etc. Everything that accepts markdown and 3 | * isn't a post. 4 | */ 5 | 6 | import { CustomEmoji } from "../components/custom-emoji"; 7 | import { compile } from "html-to-text"; 8 | import { createElement, Fragment } from "react"; 9 | import rehypeExternalLinks from "rehype-external-links"; 10 | import rehypeReact from "rehype-react"; 11 | import rehypeSanitize from "rehype-sanitize"; 12 | import rehypeStringify from "rehype-stringify"; 13 | import remarkGfm from "remark-gfm"; 14 | import remarkParse from "remark-parse"; 15 | import remarkRehype from "remark-rehype"; 16 | import { unified } from "unified"; 17 | import { Mention } from "../components/mention"; 18 | import { parseEmoji } from "./emoji"; 19 | import { chooseAgeRuleset } from "./sanitize"; 20 | import { MAX_GFM_LINES, RenderingOptions } from "./shared-types"; 21 | import { 22 | cleanUpFootnotes, 23 | convertMentions, 24 | copyImgAltToTitle, 25 | } from "./unified-processors"; 26 | import remarkBreaks from "remark-breaks"; 27 | import _ from "lodash"; 28 | 29 | const convert = compile({ 30 | wordwrap: false, 31 | }); 32 | 33 | /** 34 | * Used in places like comments, page descriptions, etc. places we don't want to 35 | * support arbitrary HTML 36 | * @returns 37 | */ 38 | const markdownRenderStackNoHTML = ( 39 | postDate: Date, 40 | lineLength: number, 41 | options: RenderingOptions 42 | ) => { 43 | let stack = unified().use(remarkParse); 44 | 45 | const ruleset = chooseAgeRuleset(postDate); 46 | 47 | if (ruleset.singleLineBreaks) { 48 | stack = stack.use(remarkBreaks); 49 | } 50 | 51 | const externalRel = ["nofollow"]; 52 | if (options.externalLinksInNewTab) { 53 | externalRel.push("noopener"); 54 | } 55 | 56 | if (lineLength < MAX_GFM_LINES) { 57 | stack = stack.use(remarkGfm, { 58 | singleTilde: false, 59 | }); 60 | } 61 | 62 | const effectiveSchema = { ...ruleset.schema }; 63 | 64 | if (options.renderingContext === "ask" && !ruleset.ask.allowEmbeddedMedia) { 65 | effectiveSchema.tagNames = _.filter( 66 | effectiveSchema.tagNames, 67 | (tagName) => !["img", "picture", "audio", "video"].includes(tagName) 68 | ); 69 | } 70 | 71 | return stack 72 | .use(remarkRehype) 73 | .use(() => copyImgAltToTitle) 74 | .use(() => cleanUpFootnotes) 75 | .use(rehypeSanitize, effectiveSchema) 76 | .use(() => ruleset.additionalVisitor) 77 | .use(rehypeExternalLinks, { 78 | rel: externalRel, 79 | target: options.externalLinksInNewTab ? "_blank" : "_self", 80 | }); 81 | }; 82 | 83 | export function renderMarkdownNoHTML( 84 | src: string, 85 | publishDate: Date, 86 | options: RenderingOptions 87 | ): string { 88 | const lineLength = src.split("\n", MAX_GFM_LINES).length; 89 | return markdownRenderStackNoHTML(publishDate, lineLength, options) 90 | .use(rehypeStringify) 91 | .processSync(src) 92 | .toString(); 93 | } 94 | 95 | export function renderSummaryNoHTML( 96 | src: string, 97 | publishDate: Date, 98 | options: RenderingOptions 99 | ): string { 100 | const renderedBody = renderMarkdownNoHTML(src, publishDate, options); 101 | return convert(renderedBody); 102 | } 103 | 104 | export function renderMarkdownReactNoHTML( 105 | src: string, 106 | publishDate: Date, 107 | options: RenderingOptions 108 | ) { 109 | const components = { 110 | Mention, 111 | CustomEmoji, 112 | }; 113 | 114 | if (options.renderingContext === "artistAlley") { 115 | // remove headers for artist alley 116 | Object.assign(components, { 117 | h1: "strong", 118 | h2: "strong", 119 | h3: "strong", 120 | h4: "strong", 121 | h5: "strong", 122 | h6: "strong", 123 | }); 124 | } 125 | 126 | const lineLength = src.split("\n", MAX_GFM_LINES).length; 127 | return ( 128 | markdownRenderStackNoHTML(publishDate, lineLength, options) 129 | .use(() => convertMentions) 130 | .use(parseEmoji, { cohostPlus: options.hasCohostPlus }) 131 | // @ts-expect-error rehype-react types are broken 132 | .use(rehypeReact, { 133 | createElement, 134 | Fragment, 135 | components, 136 | }) 137 | .processSync(src).result 138 | ); 139 | } 140 | -------------------------------------------------------------------------------- /src/lib/unified-processors.ts: -------------------------------------------------------------------------------- 1 | import type { Element, Root, Text } from "hast"; 2 | import _ from "lodash"; 3 | import { Plugin } from "unified"; 4 | import { is } from "unist-util-is"; 5 | import { CONTINUE, SKIP, visit } from "unist-util-visit"; 6 | import { EXIT, visitParents } from "unist-util-visit-parents"; 7 | import { extractMentions } from "./mention-parsing"; 8 | 9 | export const processMatches = 10 | ( 11 | regex: RegExp, 12 | callback: ( 13 | matches: string[], 14 | splits: string[], 15 | node: Text, 16 | index: number, 17 | parent: Element | Root 18 | ) => void 19 | ) => 20 | (hast: Root) => { 21 | // we only want to check on text nodes for this 22 | visit(hast, "text", (node, index, parent) => { 23 | // there is no such thing as a text node without a parent. 24 | // but if there is we want nothing to do with it. 25 | if (parent == null || index == null) return; 26 | 27 | const matches = node.value.match(regex); 28 | 29 | // if this text has mentions, process them 30 | if (matches) { 31 | const splits = node.value.split(regex); 32 | if (splits.length - 1 !== matches.length) { 33 | // something isn't how it should be. bail. 34 | return; 35 | } 36 | 37 | return callback(matches, splits, node, index, parent); 38 | } 39 | }); 40 | }; 41 | 42 | export const convertMentions = (hast: Root) => { 43 | // we only want to check on text nodes for this 44 | visit(hast, "text", (node, index, parent) => { 45 | // there is no such thing as a text node without a parent. 46 | // but if there is we want nothing to do with it. 47 | if (parent == null || index == null) return; 48 | 49 | const text = node.value; 50 | const names = extractMentions(text); 51 | 52 | // if this text has mentions, consider processing them 53 | if (names.length) { 54 | // if we have an `a` in our parent tree, we don't want to process 55 | // the mention so that links still work. 56 | let hasAnchorParent = false; 57 | visitParents(parent, { type: "text" }, (newNode, ancestors) => { 58 | // since we have to traverse for all text nodes on the parent, 59 | // we will pretty often get text nodes that _aren't_ the one 60 | // we're trying to operate on. check to make sure they match. 61 | if (!_.isEqual(node, newNode)) return CONTINUE; 62 | 63 | // flag if we've got an anchro parent 64 | hasAnchorParent = !!ancestors.find((el) => 65 | is(el, { type: "element", tagName: "a" }) 66 | ); 67 | 68 | // if we do, we don't need to check everything else so bail out. 69 | if (hasAnchorParent) return EXIT; 70 | }); 71 | 72 | if (hasAnchorParent) return CONTINUE; 73 | 74 | const els: Array = []; 75 | let currentStart = 0; 76 | 77 | names.forEach((token, idx, names) => { 78 | const [startPosition, endPosition] = token.indices; 79 | els.push({ 80 | type: "text", 81 | value: text.slice(currentStart, startPosition), 82 | }); 83 | els.push({ 84 | type: "element", 85 | tagName: "Mention", 86 | properties: { 87 | handle: token.handle, 88 | }, 89 | children: [ 90 | { 91 | type: "text", 92 | value: `@${token.handle}`, 93 | }, 94 | ], 95 | }); 96 | currentStart = endPosition; 97 | 98 | if (idx === names.length - 1) { 99 | // if we're last we need to grab the rest of the string 100 | els.push({ 101 | type: "text", 102 | value: text.slice(currentStart), 103 | }); 104 | } 105 | }); 106 | 107 | parent.children.splice(index, 1, ...els); 108 | // skip over all the new elements we just created 109 | return [SKIP, index + els.length]; 110 | } 111 | }); 112 | }; 113 | 114 | export const cleanUpFootnotes = (hast: Root) => { 115 | visit(hast, "element", (node, index, parent) => { 116 | if (parent == null || index == null) return; 117 | // remove the link from the superscript number 118 | if ( 119 | node.tagName === "a" && 120 | (node.properties?.id as string)?.includes("fnref") 121 | ) { 122 | parent.children.splice(index, 1, ...node.children); 123 | return [SKIP, index]; 124 | } 125 | 126 | // remove the little arrow at the bottom 127 | if ( 128 | node.tagName === "a" && 129 | (node.properties?.href as string)?.includes("fnref") 130 | ) { 131 | parent.children.splice(index, 1); 132 | return [SKIP, index]; 133 | } 134 | 135 | // replace the invisible label with a hr 136 | if ( 137 | node.tagName === "h2" && 138 | (node.properties?.id as string)?.includes("footnote-label") 139 | ) { 140 | const hrEl: Element = { 141 | tagName: "hr", 142 | type: "element", 143 | children: [], 144 | properties: { 145 | "aria-label": "Footnotes", 146 | style: "margin-bottom: -0.5rem;", 147 | }, 148 | }; 149 | parent.children.splice(index, 1, hrEl); 150 | } 151 | }); 152 | }; 153 | 154 | export const copyImgAltToTitle: Plugin<[], Root> = () => (hast: Root) => { 155 | visit(hast, { type: "element", tagName: "img" }, (node) => { 156 | if (node.properties?.alt) { 157 | node.properties.title = node.properties.alt; 158 | } 159 | }); 160 | }; 161 | -------------------------------------------------------------------------------- /src/types/wire-models.ts: -------------------------------------------------------------------------------- 1 | import { string, z } from "zod"; 2 | import { AccessResult, AccessResultEnum } from "./access-result"; 3 | import { 4 | AskId, 5 | AttachmentId, 6 | CommentId, 7 | ISODateString, 8 | PostId, 9 | ProjectId, 10 | UserId, 11 | } from "./ids"; 12 | import { StorageBlock, ViewBlock } from "./post-blocks"; 13 | import { PostStateEnum } from "./posts"; 14 | import { ProjectFlag, WireProjectModel } from "./projects"; 15 | 16 | // Type data needed for posts on the client 17 | export const WirePostContentCommon = z.object({ 18 | postId: PostId, 19 | headline: z.string(), 20 | publishedAt: z.string().optional(), 21 | filename: z.string(), 22 | transparentShareOfPostId: PostId.nullable(), 23 | shareOfPostId: PostId.nullable(), 24 | state: PostStateEnum, 25 | numComments: z.number(), 26 | cws: z.string().array(), 27 | tags: z.string().array(), 28 | hasCohostPlus: z.boolean(), 29 | pinned: z.boolean(), 30 | commentsLocked: z.boolean(), 31 | sharesLocked: z.boolean(), 32 | }); 33 | export type WirePostContentCommon = z.infer; 34 | 35 | export const WirePostModel = WirePostContentCommon.extend({ 36 | adultContent: z.boolean(), 37 | shareOfPostId: PostId.nullable(), 38 | updatedAt: z.string(), 39 | blocks: StorageBlock.array(), 40 | attachments: z 41 | .object({ attachmentId: AttachmentId, filename: z.string() }) 42 | .array(), 43 | }); 44 | export type WirePostModel = z.infer; 45 | 46 | // double declaration required due to a typescript limitation with recursive types 47 | // see: https://github.com/colinhacks/zod#recursive-types 48 | type WireCommentViewModelInternal = { 49 | comment: { 50 | commentId: CommentId; 51 | postedAtISO: string; 52 | deleted: boolean; 53 | body: string; 54 | children: WireCommentViewModelInternal[]; 55 | postId: PostId; 56 | inReplyTo: CommentId | null; 57 | hasCohostPlus: boolean; 58 | hidden: boolean; 59 | }; 60 | canInteract: AccessResult; 61 | canEdit: AccessResult; 62 | canHide: AccessResult; 63 | poster?: WireProjectModel; 64 | }; 65 | 66 | export const WireCommentViewModel: z.ZodType = 67 | z.lazy(() => 68 | z.object({ 69 | comment: z.object({ 70 | commentId: CommentId, 71 | postedAtISO: ISODateString, 72 | deleted: z.boolean(), 73 | body: z.string(), 74 | children: WireCommentViewModel.array(), 75 | postId: PostId, 76 | inReplyTo: CommentId.nullable(), 77 | hasCohostPlus: z.boolean(), 78 | hidden: z.boolean(), 79 | }), 80 | canInteract: AccessResultEnum, 81 | canEdit: AccessResultEnum, 82 | canHide: AccessResultEnum, 83 | poster: WireProjectModel.optional(), 84 | }) 85 | ); 86 | 87 | export type WireCommentViewModel = z.infer; 88 | 89 | export const WireRenderedPostContent = z.object({ 90 | initial: z.string(), 91 | expanded: z.string().optional(), 92 | }); 93 | 94 | export type WireRenderedPostContent = z.infer; 95 | 96 | export const LimitedVisibilityReason = z.enum([ 97 | "none", 98 | "log-in-first", 99 | "deleted", 100 | "unpublished", 101 | "adult-content", 102 | "blocked", 103 | ]); 104 | export type LimitedVisibilityReason = z.infer; 105 | 106 | // double declaration required due to a typescript limitation with recursive types 107 | // see: https://github.com/colinhacks/zod#recursive-types 108 | type WirePostViewModelInternal = WirePostContentCommon & { 109 | blocks: ViewBlock[]; 110 | 111 | plainTextBody: string; 112 | 113 | postingProject: WireProjectModel; 114 | shareTree: WirePostViewModelInternal[]; 115 | numSharedComments: number; 116 | relatedProjects: WireProjectModel[]; 117 | singlePostPageUrl: string; 118 | effectiveAdultContent: boolean; 119 | isEditor: boolean; 120 | hasAnyContributorMuted: boolean; 121 | contributorBlockIncomingOrOutgoing: boolean; 122 | postEditUrl: string; 123 | isLiked: boolean; 124 | canShare: boolean; 125 | canPublish: boolean; 126 | limitedVisibilityReason: LimitedVisibilityReason; 127 | 128 | responseToAskId: AskId | null; 129 | }; 130 | 131 | export const WirePostViewModel: z.ZodType = z.lazy( 132 | () => 133 | WirePostContentCommon.extend({ 134 | blocks: ViewBlock.array(), 135 | plainTextBody: z.string(), 136 | postingProject: WireProjectModel, 137 | shareTree: WirePostViewModel.array(), 138 | numSharedComments: z.number(), 139 | relatedProjects: WireProjectModel.array(), 140 | singlePostPageUrl: z.string().url(), 141 | effectiveAdultContent: z.boolean(), 142 | isEditor: z.boolean(), 143 | hasAnyContributorMuted: z.boolean(), 144 | contributorBlockIncomingOrOutgoing: z.boolean(), 145 | postEditUrl: z.string().url(), 146 | isLiked: z.boolean(), 147 | canShare: z.boolean(), 148 | canPublish: z.boolean(), 149 | limitedVisibilityReason: LimitedVisibilityReason, 150 | responseToAskId: AskId.nullable(), 151 | }) 152 | ); 153 | export type WirePostViewModel = z.infer; 154 | 155 | export const WireUserModel = z.object({ 156 | userId: UserId, 157 | email: z.string(), 158 | emailVerified: z.boolean(), 159 | collapseAdultContent: z.boolean(), 160 | isAdult: z.boolean(), 161 | twoFactorEnabled: z.boolean(), 162 | }); 163 | 164 | export type WireUserModel = z.infer; 165 | -------------------------------------------------------------------------------- /src/types/post-blocks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Post Blocks 3 | * 4 | * Specifies all the types for the block-based Post system. 5 | * @module 6 | */ 7 | 8 | import { z } from "zod"; 9 | import { WireAskModel } from "./asks"; 10 | import { WireImageAttachmentMetadata } from "./attachments"; 11 | import { AttachmentId } from "./ids"; 12 | 13 | /** 14 | * @internal 15 | */ 16 | const BaseBlock = z.object({ 17 | type: z.string(), 18 | }); 19 | interface BaseBlock { 20 | type: string; 21 | } 22 | 23 | /** 24 | * @category Storage Blocks 25 | * 26 | * Blocks as they are stored in the database. 27 | * Only contains minimal information needed to reproduce content, used as the base to generate [[View Blocks]]. 28 | */ 29 | 30 | /** 31 | * @category Storage Blocks 32 | */ 33 | export const MarkdownStorageBlock = BaseBlock.extend({ 34 | type: z.literal("markdown"), 35 | markdown: z.object({ 36 | /** Raw markdown to be parsed at render-time. */ 37 | content: z.string(), 38 | }), 39 | }); 40 | export type MarkdownStorageBlock = z.infer; 41 | 42 | /** 43 | * @category Storage Blocks 44 | */ 45 | export const AttachmentStorageBlock = BaseBlock.extend({ 46 | type: z.literal("attachment"), 47 | attachment: z.object({ 48 | /** ID for the [[`Attachment`]] to be rendered. */ 49 | attachmentId: AttachmentId, 50 | altText: z.string().optional(), 51 | }), 52 | }); 53 | export type AttachmentStorageBlock = z.infer; 54 | 55 | export const AttachmentRowStorageBlock = BaseBlock.extend({ 56 | type: z.literal("attachment-row"), 57 | attachments: z.array(AttachmentStorageBlock), 58 | }); 59 | export type AttachmentRowStorageBlock = z.infer< 60 | typeof AttachmentRowStorageBlock 61 | >; 62 | 63 | /** 64 | * Union type used on the [[`Post`]] model 65 | * 66 | * @category Storage Blocks 67 | */ 68 | export const StorageBlock = z.union([ 69 | MarkdownStorageBlock, 70 | AttachmentStorageBlock, 71 | AttachmentRowStorageBlock, 72 | ]); 73 | export type StorageBlock = z.infer; 74 | 75 | /** 76 | * @category View Blocks 77 | * View Blocks _must_ contain all data needed to render the block. 78 | * This is a wire-safe type and as a result _must_ not contain anything the client can't see. 79 | * 80 | */ 81 | 82 | /** 83 | * No changes are currently required from the [[`MarkdownStorageBlock`]] 84 | * so this is currently a simple alias. 85 | * 86 | * @category View Blocks 87 | * */ 88 | export const MarkdownViewBlock = MarkdownStorageBlock.extend({}); 89 | export type MarkdownViewBlock = z.infer; 90 | 91 | /** 92 | * Adds the image URL for rendering 93 | * 94 | * @category View Blocks 95 | */ 96 | const ImageAttachmentViewModel = AttachmentStorageBlock.shape.attachment 97 | .extend({ 98 | previewURL: z.string(), 99 | fileURL: z.string(), 100 | kind: z.literal("image"), 101 | width: z.number().nullish(), 102 | height: z.number().nullish(), 103 | }) 104 | .extend(WireImageAttachmentMetadata.shape); 105 | 106 | const AudioAttachmentViewModel = AttachmentStorageBlock.shape.attachment.extend( 107 | { 108 | previewURL: z.string(), 109 | fileURL: z.string(), 110 | kind: z.literal("audio"), 111 | artist: z.string().optional(), 112 | title: z.string().optional(), 113 | } 114 | ); 115 | 116 | const AttachmentViewModel = z.discriminatedUnion("kind", [ 117 | ImageAttachmentViewModel, 118 | AudioAttachmentViewModel, 119 | ]); 120 | 121 | export const AttachmentViewBlock = AttachmentStorageBlock.extend({ 122 | attachment: AttachmentViewModel, 123 | }); 124 | 125 | export type AttachmentViewBlock = z.infer; 126 | 127 | export const AttachmentRowViewBlock = AttachmentRowStorageBlock.extend({ 128 | attachments: z.array(AttachmentViewBlock), 129 | }); 130 | export type AttachmentRowViewBlock = z.infer; 131 | 132 | /** 133 | * NOTE: AskViewBlock DOES NOT have a corresponding storage block. It is 134 | * generated by the server. 135 | */ 136 | export const AskViewBlock = BaseBlock.extend({ 137 | type: z.literal("ask"), 138 | ask: WireAskModel, 139 | }); 140 | export type AskViewBlock = z.infer; 141 | 142 | /** 143 | * Union type used for [[`PostViewModel`]] and component renderers. 144 | * @category View Blocks 145 | */ 146 | export const ViewBlock = z.union([ 147 | MarkdownViewBlock, 148 | AttachmentViewBlock, 149 | AskViewBlock, 150 | AttachmentRowViewBlock, 151 | ]); 152 | export type ViewBlock = z.infer; 153 | 154 | export function isAttachmentViewBlock( 155 | test: unknown 156 | ): test is AttachmentViewBlock { 157 | return AttachmentViewBlock.safeParse(test).success; 158 | } 159 | 160 | export function isMarkdownViewBlock(test: unknown): test is MarkdownViewBlock { 161 | return MarkdownViewBlock.safeParse(test).success; 162 | } 163 | 164 | export function isAskViewBlock(test: unknown): test is AskViewBlock { 165 | return AskViewBlock.safeParse(test).success; 166 | } 167 | 168 | export function isAttachmentRowViewBlock( 169 | test: unknown 170 | ): test is AttachmentRowViewBlock { 171 | return AttachmentRowViewBlock.safeParse(test).success; 172 | } 173 | 174 | export function isAttachmentStorageBlock( 175 | test: unknown 176 | ): test is AttachmentStorageBlock { 177 | return AttachmentStorageBlock.safeParse(test).success; 178 | } 179 | 180 | export function isAttachmentRowStorageBlock( 181 | test: unknown 182 | ): test is AttachmentRowStorageBlock { 183 | return AttachmentRowStorageBlock.safeParse(test).success; 184 | } 185 | 186 | export function isMarkdownStorageBlock( 187 | test: unknown 188 | ): test is MarkdownStorageBlock { 189 | return MarkdownStorageBlock.safeParse(test).success; 190 | } 191 | 192 | // via https://github.com/colinhacks/zod/issues/627#issuecomment-911679836 193 | // could be worth investigating a factory function? 194 | export function parseAttachmentViewBlocks(originalBlocks: unknown[]) { 195 | return z 196 | .preprocess( 197 | (blocks) => z.array(z.any()).parse(blocks).filter(isAttachmentViewBlock), 198 | z.array(AttachmentViewBlock) 199 | ) 200 | .parse(originalBlocks); 201 | } 202 | 203 | export function summaryContent(block: ViewBlock): string { 204 | switch (block.type) { 205 | case "markdown": 206 | return block.markdown.content; 207 | case "attachment": { 208 | const encodedFilename = block.attachment.fileURL.split("/").pop(); 209 | 210 | return encodedFilename 211 | ? `[${block.attachment.kind}: ${decodeURIComponent(encodedFilename)}]` 212 | : `[${block.attachment.kind}]`; 213 | } 214 | case "attachment-row": 215 | return block.attachments 216 | .map((attachment) => summaryContent(attachment)) 217 | .join("\n"); 218 | case "ask": { 219 | const askerString = block.ask.anon 220 | ? block.ask.loggedIn 221 | ? "Anonymous User asked:" 222 | : "Anonymous Guest asked:" 223 | : `@${block.ask.askingProject.handle} asked:`; 224 | return `${askerString} 225 | > ${block.ask.content.split("\n").join("\n> ")}`; 226 | } 227 | } 228 | } 229 | 230 | export function getAttachmentViewBlocks( 231 | blocks: ViewBlock[] 232 | ): AttachmentViewBlock[] { 233 | return blocks.reduce((blocks, block) => { 234 | if (isAttachmentViewBlock(block)) { 235 | return [...blocks, block]; 236 | } 237 | 238 | if (isAttachmentRowViewBlock(block)) { 239 | return [...blocks, ...block.attachments]; 240 | } 241 | 242 | return blocks; 243 | }, []); 244 | } 245 | -------------------------------------------------------------------------------- /src/lib/sanitize.tsx: -------------------------------------------------------------------------------- 1 | // this file is a .tsx so that tailwind will pick up on it 2 | 3 | import deepmerge from "deepmerge"; 4 | import type { Root } from "hast"; 5 | import { Schema } from "hast-util-sanitize"; 6 | import { noop } from "lodash"; 7 | import { defaultSchema } from "rehype-sanitize"; 8 | import parseStyle from "style-to-object"; 9 | import { visit } from "unist-util-visit"; 10 | 11 | type AgeRuleset = { 12 | schema: Schema; 13 | cutoffDate: Date | null; 14 | className: string; 15 | additionalVisitor: (hast: Root) => void; 16 | singleLineBreaks: boolean; 17 | forceAttachmentsToTop: boolean; 18 | attachmentLayoutBehavior: "v1" | "v2"; 19 | ask: { 20 | allowEmbeddedMedia: boolean; 21 | }; 22 | }; 23 | 24 | const FIRST_AGE: AgeRuleset = { 25 | schema: deepmerge(defaultSchema, { 26 | attributes: { 27 | "*": ["style"], 28 | }, 29 | tagNames: ["video", "audio", "aside"], // consistency with current rules, 30 | }), 31 | // Wednesday, June 29, 2022 6:00:00 PM GMT 32 | cutoffDate: new Date(1656525600000), 33 | className: "", 34 | additionalVisitor: noop, 35 | singleLineBreaks: false, 36 | forceAttachmentsToTop: true, 37 | attachmentLayoutBehavior: "v1", 38 | ask: { 39 | allowEmbeddedMedia: true, 40 | }, 41 | }; 42 | 43 | const SECOND_AGE: AgeRuleset = { 44 | schema: deepmerge(defaultSchema, { 45 | attributes: { 46 | "*": ["style"], 47 | }, 48 | tagNames: ["video", "audio", "aside"], // consistency with current rules, 49 | }), 50 | // Monday, November 14, 2022 6:00:00 AM GMT 51 | cutoffDate: new Date(1668405600000), 52 | className: "isolate", 53 | additionalVisitor(hast) { 54 | visit(hast, "element", (node, index, parent) => { 55 | if (parent === null || index === null) return; 56 | 57 | if (node.properties?.style && typeof node.properties.style === "string") { 58 | try { 59 | let changed = false; 60 | const parsed = parseStyle(node.properties.style); 61 | if ( 62 | parsed && 63 | parsed["position"] && 64 | [ 65 | // every valid value of `position` _except_ `fixed` 66 | // (https://developer.mozilla.org/en-US/docs/Web/CSS/position), 67 | // which we disallow 68 | "static", 69 | "relative", 70 | "absolute", 71 | "sticky", 72 | "inherit", 73 | "initial", 74 | "revert", 75 | "revert-layer", 76 | "unset", 77 | ].indexOf(parsed["position"].toLowerCase()) === -1 78 | ) { 79 | parsed.position = "static"; 80 | changed = true; 81 | } 82 | 83 | if (parsed && changed) { 84 | node.properties.style = Object.entries(parsed) 85 | .map(([k, v]) => `${k}:${v}`) 86 | .join(";"); 87 | } 88 | } catch (e) { 89 | // couldn't parse, don't worry about it 90 | return; 91 | } 92 | } 93 | }); 94 | }, 95 | singleLineBreaks: false, 96 | forceAttachmentsToTop: true, 97 | attachmentLayoutBehavior: "v1", 98 | ask: { 99 | allowEmbeddedMedia: true, 100 | }, 101 | }; 102 | 103 | const THIRD_AGE: AgeRuleset = { 104 | schema: deepmerge(defaultSchema, { 105 | attributes: { 106 | "*": ["style"], 107 | }, 108 | tagNames: ["video", "audio", "aside"], // consistency with current rules, 109 | }), 110 | // may 10 2023, 3pm EDT 111 | cutoffDate: new Date("2023-05-10T15:00:00-04:00"), 112 | className: "isolate co-contain-paint", 113 | additionalVisitor(hast) { 114 | // run the previous age's visitor first 115 | SECOND_AGE.additionalVisitor(hast); 116 | 117 | visit(hast, "element", (node, index, parent) => { 118 | if (parent === null || index === null) return; 119 | 120 | if (node.properties?.style && typeof node.properties.style === "string") { 121 | try { 122 | let changed = false; 123 | const parsed = parseStyle(node.properties.style); 124 | 125 | if (parsed) { 126 | for (const key in parsed) { 127 | // drop all CSS variables 128 | if (key.startsWith("--")) { 129 | delete parsed[key]; 130 | changed = true; 131 | } 132 | } 133 | } 134 | 135 | if (parsed && changed) { 136 | node.properties.style = Object.entries(parsed) 137 | .map(([k, v]) => `${k}:${v}`) 138 | .join(";"); 139 | } 140 | } catch (e) { 141 | // couldn't parse, don't worry about it 142 | return; 143 | } 144 | } 145 | }); 146 | }, 147 | singleLineBreaks: false, 148 | forceAttachmentsToTop: true, 149 | attachmentLayoutBehavior: "v1", 150 | ask: { 151 | allowEmbeddedMedia: true, 152 | }, 153 | }; 154 | 155 | const FOURTH_AGE: AgeRuleset = { 156 | // no schema changes from third age 157 | schema: THIRD_AGE.schema, 158 | // july 17 2023, noon EDT 159 | cutoffDate: new Date("2023-07-17T12:00:00-04:00"), 160 | className: THIRD_AGE.className, 161 | additionalVisitor: THIRD_AGE.additionalVisitor, 162 | singleLineBreaks: true, 163 | forceAttachmentsToTop: true, 164 | attachmentLayoutBehavior: "v1", 165 | ask: { 166 | allowEmbeddedMedia: true, 167 | }, 168 | }; 169 | 170 | // list pulled from dompurify 171 | const MATH_ML_SCHEMA: Schema = { 172 | // allow mathml 173 | tagNames: [ 174 | "math", 175 | "menclose", 176 | "merror", 177 | "mfenced", 178 | "mfrac", 179 | "mglyph", 180 | "mi", 181 | "mlabeledtr", 182 | "mmultiscripts", 183 | "mn", 184 | "mo", 185 | "mover", 186 | "mpadded", 187 | "mphantom", 188 | "mroot", 189 | "mrow", 190 | "ms", 191 | "mspace", 192 | "msqrt", 193 | "mstyle", 194 | "msub", 195 | "msup", 196 | "msubsup", 197 | "mtable", 198 | "mtd", 199 | "mtext", 200 | "mtr", 201 | "munder", 202 | "munderover", 203 | "mprescripts", 204 | ], 205 | attributes: { 206 | "*": [ 207 | "accent", 208 | "accentunder", 209 | "align", 210 | "bevelled", 211 | "close", 212 | "columnsalign", 213 | "columnlines", 214 | "columnspan", 215 | "denomalign", 216 | "depth", 217 | "dir", 218 | "display", 219 | "displaystyle", 220 | "encoding", 221 | "fence", 222 | "frame", 223 | "height", 224 | "href", 225 | "id", 226 | "largeop", 227 | "length", 228 | "linethickness", 229 | "lspace", 230 | "lquote", 231 | "mathbackground", 232 | "mathcolor", 233 | "mathsize", 234 | "mathvariant", 235 | "maxsize", 236 | "minsize", 237 | "movablelimits", 238 | "notation", 239 | "numalign", 240 | "open", 241 | "rowalign", 242 | "rowlines", 243 | "rowspacing", 244 | "rowspan", 245 | "rspace", 246 | "rquote", 247 | "scriptlevel", 248 | "scriptminsize", 249 | "scriptsizemultiplier", 250 | "selection", 251 | "separator", 252 | "separators", 253 | "stretchy", 254 | "subscriptshift", 255 | "supscriptshift", 256 | "symmetric", 257 | "voffset", 258 | "width", 259 | "xmlns", 260 | ], 261 | span: [["className", "math-inline"]], 262 | div: [["className", "math-display"]], 263 | }, 264 | }; 265 | 266 | const FIFTH_AGE: AgeRuleset = { 267 | // we allow mathml now 268 | schema: deepmerge(FOURTH_AGE.schema, MATH_ML_SCHEMA), 269 | cutoffDate: new Date("2024-02-12T12:00:00-08:00"), // 2024/02/12 12:00 PST 270 | className: FOURTH_AGE.className, 271 | additionalVisitor: FOURTH_AGE.additionalVisitor, 272 | singleLineBreaks: true, 273 | forceAttachmentsToTop: true, 274 | attachmentLayoutBehavior: "v1", 275 | ask: { 276 | allowEmbeddedMedia: true, 277 | }, 278 | }; 279 | 280 | const SIXTH_AGE: AgeRuleset = { 281 | schema: FIFTH_AGE.schema, 282 | cutoffDate: new Date("2024-03-27T12:00:00-08:00"), // 2024/03/27 12:00 PDT 283 | className: FIFTH_AGE.className, 284 | additionalVisitor: FIFTH_AGE.additionalVisitor, 285 | singleLineBreaks: true, 286 | // attachments now render in post order instead of being pushed to the 287 | // top of the post 288 | forceAttachmentsToTop: false, 289 | attachmentLayoutBehavior: "v1", 290 | ask: { 291 | allowEmbeddedMedia: true, 292 | }, 293 | }; 294 | 295 | const SEVENTH_AGE: AgeRuleset = { 296 | schema: SIXTH_AGE.schema, 297 | cutoffDate: new Date("2024-03-29T12:00:00-08:00"), // 2024/03/29 12:00 PDT 298 | className: SIXTH_AGE.className, 299 | additionalVisitor: SIXTH_AGE.additionalVisitor, 300 | singleLineBreaks: true, 301 | forceAttachmentsToTop: false, 302 | attachmentLayoutBehavior: "v2", 303 | ask: { 304 | allowEmbeddedMedia: true, 305 | }, 306 | }; 307 | 308 | const EIGHTH_AGE: AgeRuleset = { 309 | schema: SEVENTH_AGE.schema, 310 | cutoffDate: null, // current age 311 | className: SEVENTH_AGE.className, 312 | additionalVisitor: SEVENTH_AGE.additionalVisitor, 313 | singleLineBreaks: true, 314 | forceAttachmentsToTop: false, 315 | attachmentLayoutBehavior: "v2", 316 | // asks no longer allow images, video, or audio to be embedded. 317 | ask: { 318 | allowEmbeddedMedia: false, 319 | }, 320 | }; 321 | 322 | export const AGE_LIST = [ 323 | FIRST_AGE, 324 | SECOND_AGE, 325 | THIRD_AGE, 326 | FOURTH_AGE, 327 | FIFTH_AGE, 328 | SIXTH_AGE, 329 | SEVENTH_AGE, 330 | EIGHTH_AGE, 331 | ] as const; 332 | 333 | export const chooseAgeRuleset = (postDate: Date) => 334 | AGE_LIST.find((ruleset) => { 335 | if (ruleset.cutoffDate) { 336 | return postDate < ruleset.cutoffDate; 337 | } 338 | 339 | return true; 340 | }) ?? AGE_LIST[AGE_LIST.length - 1]; 341 | -------------------------------------------------------------------------------- /src/lib/post-rendering.tsx: -------------------------------------------------------------------------------- 1 | import { InfoBox } from "../components/info-box"; 2 | import { CustomEmoji } from "../components/custom-emoji"; 3 | import { 4 | isAskViewBlock, 5 | isMarkdownViewBlock, 6 | MarkdownViewBlock, 7 | summaryContent, 8 | ViewBlock, 9 | } from "../types/post-blocks"; 10 | import { WirePostViewModel } from "../types/wire-models"; 11 | import { compile } from "html-to-text"; 12 | import { DateTime } from "luxon"; 13 | import React, { createElement, Fragment, JSX } from "react"; 14 | import rehypeExternalLinks from "rehype-external-links"; 15 | import rehypeRaw from "rehype-raw"; 16 | import rehypeReact from "rehype-react"; 17 | import rehypeSanitize from "rehype-sanitize"; 18 | import rehypeStringify from "rehype-stringify"; 19 | import remarkBreaks from "remark-breaks"; 20 | import remarkGfm from "remark-gfm"; 21 | import remarkParse from "remark-parse"; 22 | import remarkRehype from "remark-rehype"; 23 | import { Plugin, Processor, unified } from "unified"; 24 | import { Mention } from "../components/mention"; 25 | import { parseEmoji } from "./emoji"; 26 | import { chooseAgeRuleset } from "./sanitize"; 27 | import { MAX_GFM_LINES, RenderingOptions } from "./shared-types"; 28 | import { 29 | cleanUpFootnotes, 30 | convertMentions, 31 | copyImgAltToTitle, 32 | } from "./unified-processors"; 33 | import _ from "lodash"; 34 | import type { Root as HASTRoot } from "hast"; 35 | import type { Root as MDASTRoot } from "mdast"; 36 | 37 | const convert = compile({ 38 | wordwrap: false, 39 | }); 40 | 41 | /** 42 | * Used for posts only, supports arbitrary HTML 43 | * @returns 44 | */ 45 | const markdownRenderStack = ( 46 | postDate: Date, 47 | lineLength: number, 48 | options: Pick 49 | ) => { 50 | let stack = unified().use(remarkParse); 51 | 52 | const ruleset = chooseAgeRuleset(postDate); 53 | 54 | if (ruleset.singleLineBreaks) { 55 | stack = stack.use(remarkBreaks); 56 | } 57 | 58 | if (lineLength < MAX_GFM_LINES) { 59 | stack = stack.use(remarkGfm, { 60 | singleTilde: false, 61 | }); 62 | } 63 | 64 | // make a copy so we don't accidentally modify it in-place 65 | const effectiveSchema = { ...ruleset.schema }; 66 | 67 | if (options.renderingContext === "ask" && !ruleset.ask.allowEmbeddedMedia) { 68 | effectiveSchema.tagNames = _.filter( 69 | effectiveSchema.tagNames, 70 | (tagName) => !["img", "picture", "audio", "video"].includes(tagName) 71 | ); 72 | } 73 | 74 | return stack 75 | .use(remarkRehype, { 76 | allowDangerousHtml: true, 77 | }) 78 | .use(copyImgAltToTitle) 79 | .use(() => cleanUpFootnotes) 80 | .use(rehypeRaw) 81 | .use(rehypeSanitize, effectiveSchema) 82 | .use(() => ruleset.additionalVisitor); 83 | }; 84 | 85 | const ERROR_BOX_NODE = ( 86 | 87 |

88 | There was an issue rendering the HTML for this post! This usually means 89 | you've messed up syntax on a style attribute. Please check 90 | your syntax! 91 |

92 |
93 | ); 94 | 95 | async function renderMarkdownToReact( 96 | blocks: MarkdownViewBlock[], 97 | publishDate: Date, 98 | options: RenderingOptions 99 | ): Promise { 100 | const src = blocks.map((block) => block.markdown.content).join("\n\n"); 101 | let lineLength = 0; 102 | 103 | // get the max line length among the blocks. while we group all blocks 104 | // together for rendering, the performance regression associated with GFM 105 | // tables only occurs with single line breaks, which can only exist within a 106 | // single block. if the total number of line breaks ACROSS THE ENTIRE POST 107 | // is >256, this isn't an issue. we're only impacted if it's in a single 108 | // block. 109 | for (const block of blocks) { 110 | if (lineLength >= MAX_GFM_LINES) { 111 | break; 112 | } 113 | 114 | lineLength = Math.max( 115 | lineLength, 116 | block.markdown.content.split("\n", MAX_GFM_LINES).length 117 | ); 118 | } 119 | 120 | const externalRel = ["nofollow"]; 121 | if (options.externalLinksInNewTab) { 122 | externalRel.push("noopener"); 123 | } 124 | 125 | try { 126 | const result = await markdownRenderStack(publishDate, lineLength, options) 127 | .use(() => convertMentions) 128 | .use(parseEmoji, { cohostPlus: options.hasCohostPlus }) 129 | .use(rehypeExternalLinks, { 130 | rel: externalRel, 131 | target: options.externalLinksInNewTab ? "_blank" : "_self", 132 | }) 133 | // @ts-expect-error rehype-react types are broken 134 | .use(rehypeReact, { 135 | createElement, 136 | Fragment, 137 | components: { 138 | Mention, 139 | CustomEmoji, 140 | }, 141 | }) 142 | .process(src); 143 | 144 | return result.result; 145 | } catch (e) { 146 | // re-run the renderer with our static error box. we only get errors 147 | // when a user has an invalid style tag that fails parsing. our error 148 | // box is Known Good so this is not a concern for us. 149 | return ERROR_BOX_NODE; 150 | } 151 | } 152 | 153 | function renderMarkdown(src: string, publishDate: Date): string { 154 | const lineLength = src.split("\n", MAX_GFM_LINES).length; 155 | return markdownRenderStack(publishDate, lineLength, { 156 | renderingContext: "post", 157 | }) 158 | .use(rehypeStringify) 159 | .processSync(src) 160 | .toString(); 161 | } 162 | 163 | export function renderPostSummary( 164 | viewModel: WirePostViewModel, 165 | options: { myPost: boolean; rss?: boolean; skipHeadline?: boolean } 166 | ): string { 167 | // invocations with either options.rss set true, or options.skipHeadline 168 | // set true, have a second field they can use to carry the headline of the 169 | // post and don't need to have it embedded herein. 170 | const effectiveSkipHeadline = options.skipHeadline || options.rss; 171 | 172 | if (!options.myPost) { 173 | if (viewModel.effectiveAdultContent && viewModel.cws.length > 0) { 174 | const cwList = viewModel.cws.join(", "); 175 | 176 | return `18+ content; content warnings: ${cwList}`; 177 | } else if (viewModel.cws.length > 0) { 178 | const cwList = viewModel.cws.join(", "); 179 | 180 | return `(content warning: ${cwList})`; 181 | } else if (viewModel.effectiveAdultContent) { 182 | return "this post contains 18+ content"; 183 | } 184 | } 185 | 186 | if (viewModel.transparentShareOfPostId) { 187 | // transparent share; find the opaque post above it 188 | const originalPost = viewModel.shareTree.find( 189 | (vm) => vm.postId === viewModel.transparentShareOfPostId 190 | ); 191 | 192 | if (options.rss) { 193 | // RSS: just include a link in the summary 194 | if (originalPost) { 195 | return `Share from @${originalPost.postingProject.handle}: ${originalPost.singlePostPageUrl}`; 196 | } 197 | } else { 198 | // on-site: nest opaque parent's preview inside this one 199 | if (originalPost) { 200 | return `Share from @${ 201 | originalPost.postingProject.handle 202 | }: ${renderPostSummary(originalPost, options)}`; 203 | } 204 | } 205 | } 206 | 207 | let summary: string = ""; 208 | 209 | if (viewModel.headline && !effectiveSkipHeadline) { 210 | summary = viewModel.headline; 211 | } else { 212 | const effectiveDate = viewModel.publishedAt 213 | ? DateTime.fromISO(viewModel.publishedAt).toJSDate() 214 | : new Date(); 215 | 216 | const askBlocks = viewModel.blocks.filter(isAskViewBlock); 217 | const markdownBlocks = viewModel.blocks.filter(isMarkdownViewBlock); 218 | const textBlocks = [...askBlocks, ...markdownBlocks]; 219 | const textContent = (textBlocks.length > 0 ? textBlocks : viewModel.blocks) 220 | .map((block) => summaryContent(block)) 221 | .join("\n\n"); 222 | 223 | const renderedBody = renderMarkdown(textContent, effectiveDate); 224 | 225 | summary = convert(renderedBody); 226 | } 227 | 228 | if (options.rss && viewModel.shareOfPostId) { 229 | // contentful share, include the link at the start of the summary 230 | const originalPost = viewModel.shareTree.find( 231 | (vm) => vm.postId === viewModel.shareOfPostId 232 | ); 233 | if (originalPost) { 234 | summary = `Share from @${originalPost.postingProject.handle}: ${originalPost.singlePostPageUrl}\n\n${summary}`; 235 | } 236 | } 237 | 238 | return summary; 239 | } 240 | 241 | export type PostRenderResult = { 242 | spans: Array<{ 243 | startIndex: number; 244 | endIndex: number; 245 | rendered: JSX.Element; 246 | }>; 247 | readMoreIndex: number | null; 248 | }; 249 | 250 | export async function renderPostBlocks( 251 | viewBlocks: ViewBlock[], 252 | publishDate: Date, 253 | options: RenderingOptions 254 | ): Promise { 255 | // identify markdown spans 256 | const spans: { 257 | startIndex: number; 258 | endIndex: number; 259 | }[] = []; 260 | let currentSpanStartIndex: number | null = null; 261 | let readMoreIndex: number | null = null; 262 | 263 | for (let i = 0; i < viewBlocks.length; i++) { 264 | const block = viewBlocks[i]; 265 | 266 | if (isMarkdownViewBlock(block)) { 267 | if ( 268 | currentSpanStartIndex !== null && 269 | block.markdown.content === "---" && 270 | readMoreIndex === null 271 | ) { 272 | // inside a span, content is "---", no read-more yet: end the 273 | // span, set read-more index, and start a new one 274 | spans.push({ 275 | startIndex: currentSpanStartIndex, 276 | endIndex: i, 277 | }); 278 | currentSpanStartIndex = i; 279 | readMoreIndex = i; 280 | } else if (currentSpanStartIndex !== null) { 281 | // inside a span, any other content: keep it going 282 | continue; 283 | } else { 284 | // outside a span: start a new span 285 | currentSpanStartIndex = i; 286 | } 287 | } else { 288 | if (currentSpanStartIndex !== null) { 289 | // inside a span: end the span 290 | spans.push({ 291 | startIndex: currentSpanStartIndex, 292 | endIndex: i, 293 | }); 294 | currentSpanStartIndex = null; 295 | } else { 296 | // outside a span: do nothing 297 | continue; 298 | } 299 | } 300 | } 301 | 302 | // if we ended the post in a span, finish the one we were in 303 | if (currentSpanStartIndex !== null) { 304 | spans.push({ 305 | startIndex: currentSpanStartIndex, 306 | endIndex: viewBlocks.length, 307 | }); 308 | } 309 | 310 | // render each span and return rendered React elements 311 | return { 312 | spans: await Promise.all( 313 | spans.map(async (span) => ({ 314 | startIndex: span.startIndex, 315 | endIndex: span.endIndex, 316 | rendered: await renderMarkdownToReact( 317 | viewBlocks 318 | .slice(span.startIndex, span.endIndex) 319 | .filter(isMarkdownViewBlock), 320 | publishDate, 321 | options 322 | ), 323 | })) 324 | ), 325 | readMoreIndex, 326 | }; 327 | } 328 | 329 | // interim rendering method for until we get the rest of the inline attachments 330 | // changes done. render a sequence of markdown spans all joined together. 331 | export function renderReactFromSpans(spans: PostRenderResult["spans"]) { 332 | const rendered: JSX.Element[] = []; 333 | 334 | for (let i = 0; i < spans.length; i++) { 335 | // throw a warning if there are any missing blocks in the middle of the 336 | // rendered spans. this should only happen if we're attempting to 337 | // render the markdown in a post with attachments in the middle, which 338 | // shouldn't exist yet. 339 | if (i != 0 && spans[i].startIndex !== spans[i - 1].endIndex) { 340 | console.error("renderReactFromSpans: span interval is sparse?"); 341 | } 342 | 343 | rendered.push( 344 | 345 | {spans[i].rendered} 346 | 347 | ); 348 | } 349 | 350 | return <>{rendered}; 351 | } 352 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | deepmerge: 12 | specifier: ^4.3.1 13 | version: 4.3.1 14 | hast-util-sanitize: 15 | specifier: ^4.0.0 16 | version: 4.1.0 17 | html-to-text: 18 | specifier: ^9.0.5 19 | version: 9.0.5 20 | lodash: 21 | specifier: ^4.17.21 22 | version: 4.17.21 23 | luxon: 24 | specifier: ^3.7.2 25 | version: 3.7.2 26 | rehype-external-links: 27 | specifier: ^2.0.0 28 | version: 2.1.0 29 | rehype-raw: 30 | specifier: ^6.1.1 31 | version: 6.1.1 32 | rehype-react: 33 | specifier: ^7.1.1 34 | version: 7.2.0(@types/react@18.3.26) 35 | rehype-remark: 36 | specifier: ^9.1.2 37 | version: 9.1.2 38 | rehype-sanitize: 39 | specifier: ^5.0.1 40 | version: 5.0.1 41 | rehype-stringify: 42 | specifier: ^9.0.3 43 | version: 9.0.4 44 | remark: 45 | specifier: ^15.0.1 46 | version: 15.0.1 47 | remark-breaks: 48 | specifier: ^3.0.3 49 | version: 3.0.3 50 | remark-gfm: 51 | specifier: ^3.0.1 52 | version: 3.0.1 53 | remark-parse: 54 | specifier: ^10.0.1 55 | version: 10.0.2 56 | remark-rehype: 57 | specifier: ^10.1.0 58 | version: 10.1.0 59 | remark-stringify: 60 | specifier: ^10.0.2 61 | version: 10.0.3 62 | style-to-object: 63 | specifier: ^1.0.11 64 | version: 1.0.11 65 | unified: 66 | specifier: ^10.1.2 67 | version: 10.1.2 68 | unist-builder: 69 | specifier: ^3.0.0 70 | version: 3.0.1 71 | unist-util-is: 72 | specifier: ^5.2.0 73 | version: 5.2.1 74 | unist-util-visit: 75 | specifier: ^4.1.0 76 | version: 4.1.2 77 | unist-util-visit-parents: 78 | specifier: ^5.1.3 79 | version: 5.1.3 80 | zod: 81 | specifier: ^4.1.12 82 | version: 4.1.12 83 | devDependencies: 84 | '@rollup/plugin-typescript': 85 | specifier: ^12.1.4 86 | version: 12.1.4(rollup@4.52.4)(tslib@2.8.1)(typescript@5.9.3) 87 | '@types/hast': 88 | specifier: ^2.3.4 89 | version: 2.3.10 90 | '@types/html-to-text': 91 | specifier: ^9.0.4 92 | version: 9.0.4 93 | '@types/lodash': 94 | specifier: ^4.17.20 95 | version: 4.17.20 96 | '@types/luxon': 97 | specifier: ^3.7.1 98 | version: 3.7.1 99 | '@types/mdast': 100 | specifier: ^3.0.10 101 | version: 3.0.15 102 | '@types/react': 103 | specifier: ^18 104 | version: 18.3.26 105 | react: 106 | specifier: ^18 107 | version: 18.3.1 108 | rollup: 109 | specifier: ^4.52.4 110 | version: 4.52.4 111 | tslib: 112 | specifier: ^2.8.1 113 | version: 2.8.1 114 | typescript: 115 | specifier: ^5.9.3 116 | version: 5.9.3 117 | 118 | packages: 119 | 120 | '@mapbox/hast-util-table-cell-style@0.2.1': 121 | resolution: {integrity: sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==} 122 | engines: {node: '>=12'} 123 | 124 | '@rollup/plugin-typescript@12.1.4': 125 | resolution: {integrity: sha512-s5Hx+EtN60LMlDBvl5f04bEiFZmAepk27Q+mr85L/00zPDn1jtzlTV6FWn81MaIwqfWzKxmOJrBWHU6vtQyedQ==} 126 | engines: {node: '>=14.0.0'} 127 | peerDependencies: 128 | rollup: ^2.14.0||^3.0.0||^4.0.0 129 | tslib: '*' 130 | typescript: '>=3.7.0' 131 | peerDependenciesMeta: 132 | rollup: 133 | optional: true 134 | tslib: 135 | optional: true 136 | 137 | '@rollup/pluginutils@5.3.0': 138 | resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} 139 | engines: {node: '>=14.0.0'} 140 | peerDependencies: 141 | rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 142 | peerDependenciesMeta: 143 | rollup: 144 | optional: true 145 | 146 | '@rollup/rollup-android-arm-eabi@4.52.4': 147 | resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} 148 | cpu: [arm] 149 | os: [android] 150 | 151 | '@rollup/rollup-android-arm64@4.52.4': 152 | resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==} 153 | cpu: [arm64] 154 | os: [android] 155 | 156 | '@rollup/rollup-darwin-arm64@4.52.4': 157 | resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==} 158 | cpu: [arm64] 159 | os: [darwin] 160 | 161 | '@rollup/rollup-darwin-x64@4.52.4': 162 | resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==} 163 | cpu: [x64] 164 | os: [darwin] 165 | 166 | '@rollup/rollup-freebsd-arm64@4.52.4': 167 | resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==} 168 | cpu: [arm64] 169 | os: [freebsd] 170 | 171 | '@rollup/rollup-freebsd-x64@4.52.4': 172 | resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==} 173 | cpu: [x64] 174 | os: [freebsd] 175 | 176 | '@rollup/rollup-linux-arm-gnueabihf@4.52.4': 177 | resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} 178 | cpu: [arm] 179 | os: [linux] 180 | 181 | '@rollup/rollup-linux-arm-musleabihf@4.52.4': 182 | resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} 183 | cpu: [arm] 184 | os: [linux] 185 | 186 | '@rollup/rollup-linux-arm64-gnu@4.52.4': 187 | resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} 188 | cpu: [arm64] 189 | os: [linux] 190 | 191 | '@rollup/rollup-linux-arm64-musl@4.52.4': 192 | resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} 193 | cpu: [arm64] 194 | os: [linux] 195 | 196 | '@rollup/rollup-linux-loong64-gnu@4.52.4': 197 | resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} 198 | cpu: [loong64] 199 | os: [linux] 200 | 201 | '@rollup/rollup-linux-ppc64-gnu@4.52.4': 202 | resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} 203 | cpu: [ppc64] 204 | os: [linux] 205 | 206 | '@rollup/rollup-linux-riscv64-gnu@4.52.4': 207 | resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} 208 | cpu: [riscv64] 209 | os: [linux] 210 | 211 | '@rollup/rollup-linux-riscv64-musl@4.52.4': 212 | resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} 213 | cpu: [riscv64] 214 | os: [linux] 215 | 216 | '@rollup/rollup-linux-s390x-gnu@4.52.4': 217 | resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} 218 | cpu: [s390x] 219 | os: [linux] 220 | 221 | '@rollup/rollup-linux-x64-gnu@4.52.4': 222 | resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} 223 | cpu: [x64] 224 | os: [linux] 225 | 226 | '@rollup/rollup-linux-x64-musl@4.52.4': 227 | resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} 228 | cpu: [x64] 229 | os: [linux] 230 | 231 | '@rollup/rollup-openharmony-arm64@4.52.4': 232 | resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} 233 | cpu: [arm64] 234 | os: [openharmony] 235 | 236 | '@rollup/rollup-win32-arm64-msvc@4.52.4': 237 | resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==} 238 | cpu: [arm64] 239 | os: [win32] 240 | 241 | '@rollup/rollup-win32-ia32-msvc@4.52.4': 242 | resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==} 243 | cpu: [ia32] 244 | os: [win32] 245 | 246 | '@rollup/rollup-win32-x64-gnu@4.52.4': 247 | resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==} 248 | cpu: [x64] 249 | os: [win32] 250 | 251 | '@rollup/rollup-win32-x64-msvc@4.52.4': 252 | resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==} 253 | cpu: [x64] 254 | os: [win32] 255 | 256 | '@selderee/plugin-htmlparser2@0.11.0': 257 | resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} 258 | 259 | '@types/debug@4.1.12': 260 | resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} 261 | 262 | '@types/estree@1.0.8': 263 | resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 264 | 265 | '@types/extend@3.0.4': 266 | resolution: {integrity: sha512-ArMouDUTJEz1SQRpFsT2rIw7DeqICFv5aaVzLSIYMYQSLcwcGOfT3VyglQs/p7K3F7fT4zxr0NWxYZIdifD6dA==} 267 | 268 | '@types/hast@2.3.10': 269 | resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} 270 | 271 | '@types/html-to-text@9.0.4': 272 | resolution: {integrity: sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==} 273 | 274 | '@types/lodash@4.17.20': 275 | resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} 276 | 277 | '@types/luxon@3.7.1': 278 | resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} 279 | 280 | '@types/mdast@3.0.15': 281 | resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} 282 | 283 | '@types/mdast@4.0.4': 284 | resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} 285 | 286 | '@types/ms@2.1.0': 287 | resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} 288 | 289 | '@types/parse5@6.0.3': 290 | resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} 291 | 292 | '@types/prop-types@15.7.15': 293 | resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} 294 | 295 | '@types/react@18.3.26': 296 | resolution: {integrity: sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==} 297 | 298 | '@types/unist@2.0.11': 299 | resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} 300 | 301 | '@types/unist@3.0.3': 302 | resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} 303 | 304 | bail@2.0.2: 305 | resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} 306 | 307 | ccount@2.0.1: 308 | resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} 309 | 310 | character-entities-html4@2.1.0: 311 | resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} 312 | 313 | character-entities-legacy@3.0.0: 314 | resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} 315 | 316 | character-entities@2.0.2: 317 | resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} 318 | 319 | comma-separated-tokens@2.0.3: 320 | resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} 321 | 322 | csstype@3.1.3: 323 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 324 | 325 | debug@4.4.3: 326 | resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 327 | engines: {node: '>=6.0'} 328 | peerDependencies: 329 | supports-color: '*' 330 | peerDependenciesMeta: 331 | supports-color: 332 | optional: true 333 | 334 | decode-named-character-reference@1.2.0: 335 | resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} 336 | 337 | deepmerge@4.3.1: 338 | resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 339 | engines: {node: '>=0.10.0'} 340 | 341 | dequal@2.0.3: 342 | resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 343 | engines: {node: '>=6'} 344 | 345 | devlop@1.1.0: 346 | resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} 347 | 348 | diff@5.2.0: 349 | resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} 350 | engines: {node: '>=0.3.1'} 351 | 352 | dom-serializer@2.0.0: 353 | resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} 354 | 355 | domelementtype@2.3.0: 356 | resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} 357 | 358 | domhandler@5.0.3: 359 | resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} 360 | engines: {node: '>= 4'} 361 | 362 | domutils@3.2.2: 363 | resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} 364 | 365 | entities@4.5.0: 366 | resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 367 | engines: {node: '>=0.12'} 368 | 369 | escape-string-regexp@5.0.0: 370 | resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} 371 | engines: {node: '>=12'} 372 | 373 | estree-walker@2.0.2: 374 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 375 | 376 | extend@3.0.2: 377 | resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} 378 | 379 | fsevents@2.3.3: 380 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 381 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 382 | os: [darwin] 383 | 384 | function-bind@1.1.2: 385 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 386 | 387 | hasown@2.0.2: 388 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 389 | engines: {node: '>= 0.4'} 390 | 391 | hast-to-hyperscript@10.0.3: 392 | resolution: {integrity: sha512-NuBoUStp4fRwmvlfbidlEiRSTk0gSHm+97q4Xn9CJ10HO+Py7nlTuDi6RhM1qLOureukGrCXLG7AAxaGqqyslQ==} 393 | 394 | hast-util-embedded@2.0.1: 395 | resolution: {integrity: sha512-QUdSOP1/o+/TxXtpPFXR2mUg2P+ySrmlX7QjwHZCXqMFyYk7YmcGSvqRW+4XgXAoHifdE1t2PwFaQK33TqVjSw==} 396 | 397 | hast-util-from-parse5@7.1.2: 398 | resolution: {integrity: sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==} 399 | 400 | hast-util-has-property@2.0.1: 401 | resolution: {integrity: sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg==} 402 | 403 | hast-util-is-body-ok-link@2.0.0: 404 | resolution: {integrity: sha512-S58hCexyKdD31vMsErvgLfflW6vYWo/ixRLPJTtkOvLld24vyI8vmYmkgLA5LG3la2ME7nm7dLGdm48gfLRBfw==} 405 | 406 | hast-util-is-element@2.1.3: 407 | resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==} 408 | 409 | hast-util-parse-selector@3.1.1: 410 | resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} 411 | 412 | hast-util-phrasing@2.0.2: 413 | resolution: {integrity: sha512-yGkCfPkkfCyiLfK6KEl/orMDr/zgCnq/NaO9HfULx6/Zga5fso5eqQA5Ov/JZVqACygvw9shRYWgXNcG2ilo7w==} 414 | 415 | hast-util-raw@7.2.3: 416 | resolution: {integrity: sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==} 417 | 418 | hast-util-sanitize@4.1.0: 419 | resolution: {integrity: sha512-Hd9tU0ltknMGRDv+d6Ro/4XKzBqQnP/EZrpiTbpFYfXv/uOhWeKc+2uajcbEvAEH98VZd7eII2PiXm13RihnLw==} 420 | 421 | hast-util-to-html@8.0.4: 422 | resolution: {integrity: sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==} 423 | 424 | hast-util-to-mdast@8.4.1: 425 | resolution: {integrity: sha512-tfmBLASuCgyhCzpkTXM5kU8xeuS5jkMZ17BYm2YftGT5wvgc7uHXTZ/X8WfNd6F5NV/IGmrLsuahZ+jXQir4zQ==} 426 | 427 | hast-util-to-parse5@7.1.0: 428 | resolution: {integrity: sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==} 429 | 430 | hast-util-to-text@3.1.2: 431 | resolution: {integrity: sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==} 432 | 433 | hast-util-whitespace@2.0.1: 434 | resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} 435 | 436 | hastscript@7.2.0: 437 | resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} 438 | 439 | html-to-text@9.0.5: 440 | resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} 441 | engines: {node: '>=14'} 442 | 443 | html-void-elements@2.0.1: 444 | resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} 445 | 446 | htmlparser2@8.0.2: 447 | resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} 448 | 449 | inline-style-parser@0.1.1: 450 | resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} 451 | 452 | inline-style-parser@0.2.4: 453 | resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} 454 | 455 | is-absolute-url@4.0.1: 456 | resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} 457 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 458 | 459 | is-buffer@2.0.5: 460 | resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} 461 | engines: {node: '>=4'} 462 | 463 | is-core-module@2.16.1: 464 | resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} 465 | engines: {node: '>= 0.4'} 466 | 467 | is-plain-obj@4.1.0: 468 | resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} 469 | engines: {node: '>=12'} 470 | 471 | js-tokens@4.0.0: 472 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 473 | 474 | kleur@4.1.5: 475 | resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 476 | engines: {node: '>=6'} 477 | 478 | leac@0.6.0: 479 | resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} 480 | 481 | lodash@4.17.21: 482 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 483 | 484 | longest-streak@3.1.0: 485 | resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} 486 | 487 | loose-envify@1.4.0: 488 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 489 | hasBin: true 490 | 491 | luxon@3.7.2: 492 | resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} 493 | engines: {node: '>=12'} 494 | 495 | markdown-table@3.0.4: 496 | resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} 497 | 498 | mdast-util-definitions@5.1.2: 499 | resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} 500 | 501 | mdast-util-find-and-replace@2.2.2: 502 | resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} 503 | 504 | mdast-util-from-markdown@1.3.1: 505 | resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} 506 | 507 | mdast-util-from-markdown@2.0.2: 508 | resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} 509 | 510 | mdast-util-gfm-autolink-literal@1.0.3: 511 | resolution: {integrity: sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==} 512 | 513 | mdast-util-gfm-footnote@1.0.2: 514 | resolution: {integrity: sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==} 515 | 516 | mdast-util-gfm-strikethrough@1.0.3: 517 | resolution: {integrity: sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==} 518 | 519 | mdast-util-gfm-table@1.0.7: 520 | resolution: {integrity: sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==} 521 | 522 | mdast-util-gfm-task-list-item@1.0.2: 523 | resolution: {integrity: sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==} 524 | 525 | mdast-util-gfm@2.0.2: 526 | resolution: {integrity: sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==} 527 | 528 | mdast-util-newline-to-break@1.0.0: 529 | resolution: {integrity: sha512-491LcYv3gbGhhCrLoeALncQmega2xPh+m3gbsIhVsOX4sw85+ShLFPvPyibxc1Swx/6GtzxgVodq+cGa/47ULg==} 530 | 531 | mdast-util-phrasing@3.0.1: 532 | resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} 533 | 534 | mdast-util-phrasing@4.1.0: 535 | resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} 536 | 537 | mdast-util-to-hast@12.3.0: 538 | resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==} 539 | 540 | mdast-util-to-markdown@1.5.0: 541 | resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} 542 | 543 | mdast-util-to-markdown@2.1.2: 544 | resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} 545 | 546 | mdast-util-to-string@3.2.0: 547 | resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} 548 | 549 | mdast-util-to-string@4.0.0: 550 | resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} 551 | 552 | micromark-core-commonmark@1.1.0: 553 | resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} 554 | 555 | micromark-core-commonmark@2.0.3: 556 | resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} 557 | 558 | micromark-extension-gfm-autolink-literal@1.0.5: 559 | resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==} 560 | 561 | micromark-extension-gfm-footnote@1.1.2: 562 | resolution: {integrity: sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==} 563 | 564 | micromark-extension-gfm-strikethrough@1.0.7: 565 | resolution: {integrity: sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==} 566 | 567 | micromark-extension-gfm-table@1.0.7: 568 | resolution: {integrity: sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==} 569 | 570 | micromark-extension-gfm-tagfilter@1.0.2: 571 | resolution: {integrity: sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==} 572 | 573 | micromark-extension-gfm-task-list-item@1.0.5: 574 | resolution: {integrity: sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==} 575 | 576 | micromark-extension-gfm@2.0.3: 577 | resolution: {integrity: sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==} 578 | 579 | micromark-factory-destination@1.1.0: 580 | resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} 581 | 582 | micromark-factory-destination@2.0.1: 583 | resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} 584 | 585 | micromark-factory-label@1.1.0: 586 | resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} 587 | 588 | micromark-factory-label@2.0.1: 589 | resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} 590 | 591 | micromark-factory-space@1.1.0: 592 | resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} 593 | 594 | micromark-factory-space@2.0.1: 595 | resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} 596 | 597 | micromark-factory-title@1.1.0: 598 | resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} 599 | 600 | micromark-factory-title@2.0.1: 601 | resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} 602 | 603 | micromark-factory-whitespace@1.1.0: 604 | resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} 605 | 606 | micromark-factory-whitespace@2.0.1: 607 | resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} 608 | 609 | micromark-util-character@1.2.0: 610 | resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} 611 | 612 | micromark-util-character@2.1.1: 613 | resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} 614 | 615 | micromark-util-chunked@1.1.0: 616 | resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} 617 | 618 | micromark-util-chunked@2.0.1: 619 | resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} 620 | 621 | micromark-util-classify-character@1.1.0: 622 | resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} 623 | 624 | micromark-util-classify-character@2.0.1: 625 | resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} 626 | 627 | micromark-util-combine-extensions@1.1.0: 628 | resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} 629 | 630 | micromark-util-combine-extensions@2.0.1: 631 | resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} 632 | 633 | micromark-util-decode-numeric-character-reference@1.1.0: 634 | resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} 635 | 636 | micromark-util-decode-numeric-character-reference@2.0.2: 637 | resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} 638 | 639 | micromark-util-decode-string@1.1.0: 640 | resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} 641 | 642 | micromark-util-decode-string@2.0.1: 643 | resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} 644 | 645 | micromark-util-encode@1.1.0: 646 | resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} 647 | 648 | micromark-util-encode@2.0.1: 649 | resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} 650 | 651 | micromark-util-html-tag-name@1.2.0: 652 | resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} 653 | 654 | micromark-util-html-tag-name@2.0.1: 655 | resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} 656 | 657 | micromark-util-normalize-identifier@1.1.0: 658 | resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} 659 | 660 | micromark-util-normalize-identifier@2.0.1: 661 | resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} 662 | 663 | micromark-util-resolve-all@1.1.0: 664 | resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} 665 | 666 | micromark-util-resolve-all@2.0.1: 667 | resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} 668 | 669 | micromark-util-sanitize-uri@1.2.0: 670 | resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} 671 | 672 | micromark-util-sanitize-uri@2.0.1: 673 | resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} 674 | 675 | micromark-util-subtokenize@1.1.0: 676 | resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} 677 | 678 | micromark-util-subtokenize@2.1.0: 679 | resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} 680 | 681 | micromark-util-symbol@1.1.0: 682 | resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} 683 | 684 | micromark-util-symbol@2.0.1: 685 | resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} 686 | 687 | micromark-util-types@1.1.0: 688 | resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} 689 | 690 | micromark-util-types@2.0.2: 691 | resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} 692 | 693 | micromark@3.2.0: 694 | resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} 695 | 696 | micromark@4.0.2: 697 | resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} 698 | 699 | mri@1.2.0: 700 | resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 701 | engines: {node: '>=4'} 702 | 703 | ms@2.1.3: 704 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 705 | 706 | parse5@6.0.1: 707 | resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} 708 | 709 | parseley@0.12.1: 710 | resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} 711 | 712 | path-parse@1.0.7: 713 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 714 | 715 | peberminta@0.9.0: 716 | resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} 717 | 718 | picomatch@4.0.3: 719 | resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 720 | engines: {node: '>=12'} 721 | 722 | property-information@6.5.0: 723 | resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} 724 | 725 | react@18.3.1: 726 | resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 727 | engines: {node: '>=0.10.0'} 728 | 729 | rehype-external-links@2.1.0: 730 | resolution: {integrity: sha512-2YMJZVM1hxZnwl9IPkbN5Pjn78kXkAX7lq9VEtlaGA29qIls25vZN+ucNIJdbQUe+9NNFck17BiOhGmsD6oLIg==} 731 | 732 | rehype-minify-whitespace@5.0.1: 733 | resolution: {integrity: sha512-PPp4lWJiBPlePI/dv1BeYktbwkfgXkrK59MUa+tYbMPgleod+4DvFK2PLU0O0O60/xuhHfiR9GUIUlXTU8sRIQ==} 734 | 735 | rehype-raw@6.1.1: 736 | resolution: {integrity: sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==} 737 | 738 | rehype-react@7.2.0: 739 | resolution: {integrity: sha512-MHYyCHka+3TtzBMKtcuvVOBAbI1HrfoYA+XH9m7/rlrQQATCPwtJnPdkxKKcIGF8vc9mxqQja9r9f+FHItQeWg==} 740 | peerDependencies: 741 | '@types/react': '>=17' 742 | 743 | rehype-remark@9.1.2: 744 | resolution: {integrity: sha512-c0fG3/CrJ95zAQ07xqHSkdpZybwdsY7X5dNWvgL2XqLKZuqmG3+vk6kP/4miCnp+R+x/0uKKRSpfXb9aGR8Z5w==} 745 | 746 | rehype-sanitize@5.0.1: 747 | resolution: {integrity: sha512-da/jIOjq8eYt/1r9GN6GwxIR3gde7OZ+WV8pheu1tL8K0D9KxM2AyMh+UEfke+FfdM3PvGHeYJU0Td5OWa7L5A==} 748 | 749 | rehype-stringify@9.0.4: 750 | resolution: {integrity: sha512-Uk5xu1YKdqobe5XpSskwPvo1XeHUUucWEQSl8hTrXt5selvca1e8K1EZ37E6YoZ4BT8BCqCdVfQW7OfHfthtVQ==} 751 | 752 | remark-breaks@3.0.3: 753 | resolution: {integrity: sha512-C7VkvcUp1TPUc2eAYzsPdaUh8Xj4FSbQnYA5A9f80diApLZscTDeG7efiWP65W8hV2sEy3JuGVU0i6qr5D8Hug==} 754 | 755 | remark-gfm@3.0.1: 756 | resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} 757 | 758 | remark-parse@10.0.2: 759 | resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} 760 | 761 | remark-parse@11.0.0: 762 | resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} 763 | 764 | remark-rehype@10.1.0: 765 | resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} 766 | 767 | remark-stringify@10.0.3: 768 | resolution: {integrity: sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A==} 769 | 770 | remark-stringify@11.0.0: 771 | resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} 772 | 773 | remark@15.0.1: 774 | resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} 775 | 776 | resolve@1.22.10: 777 | resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} 778 | engines: {node: '>= 0.4'} 779 | hasBin: true 780 | 781 | rollup@4.52.4: 782 | resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} 783 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 784 | hasBin: true 785 | 786 | sade@1.8.1: 787 | resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 788 | engines: {node: '>=6'} 789 | 790 | selderee@0.11.0: 791 | resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} 792 | 793 | space-separated-tokens@2.0.2: 794 | resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} 795 | 796 | stringify-entities@4.0.4: 797 | resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} 798 | 799 | style-to-object@0.4.4: 800 | resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} 801 | 802 | style-to-object@1.0.11: 803 | resolution: {integrity: sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==} 804 | 805 | supports-preserve-symlinks-flag@1.0.0: 806 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 807 | engines: {node: '>= 0.4'} 808 | 809 | trim-lines@3.0.1: 810 | resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} 811 | 812 | trim-trailing-lines@2.1.0: 813 | resolution: {integrity: sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==} 814 | 815 | trough@2.2.0: 816 | resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} 817 | 818 | tslib@2.8.1: 819 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 820 | 821 | typescript@5.9.3: 822 | resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 823 | engines: {node: '>=14.17'} 824 | hasBin: true 825 | 826 | unified@10.1.2: 827 | resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} 828 | 829 | unified@11.0.5: 830 | resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} 831 | 832 | unist-builder@3.0.1: 833 | resolution: {integrity: sha512-gnpOw7DIpCA0vpr6NqdPvTWnlPTApCTRzr+38E6hCWx3rz/cjo83SsKIlS1Z+L5ttScQ2AwutNnb8+tAvpb6qQ==} 834 | 835 | unist-util-find-after@4.0.1: 836 | resolution: {integrity: sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==} 837 | 838 | unist-util-generated@2.0.1: 839 | resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} 840 | 841 | unist-util-is@3.0.0: 842 | resolution: {integrity: sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==} 843 | 844 | unist-util-is@5.2.1: 845 | resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} 846 | 847 | unist-util-is@6.0.1: 848 | resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} 849 | 850 | unist-util-position@4.0.4: 851 | resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} 852 | 853 | unist-util-stringify-position@3.0.3: 854 | resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} 855 | 856 | unist-util-stringify-position@4.0.0: 857 | resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} 858 | 859 | unist-util-visit-parents@2.1.2: 860 | resolution: {integrity: sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==} 861 | 862 | unist-util-visit-parents@5.1.3: 863 | resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} 864 | 865 | unist-util-visit-parents@6.0.2: 866 | resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} 867 | 868 | unist-util-visit@1.4.1: 869 | resolution: {integrity: sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==} 870 | 871 | unist-util-visit@4.1.2: 872 | resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} 873 | 874 | unist-util-visit@5.0.0: 875 | resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} 876 | 877 | uvu@0.5.6: 878 | resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} 879 | engines: {node: '>=8'} 880 | hasBin: true 881 | 882 | vfile-location@4.1.0: 883 | resolution: {integrity: sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==} 884 | 885 | vfile-message@3.1.4: 886 | resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} 887 | 888 | vfile-message@4.0.3: 889 | resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} 890 | 891 | vfile@5.3.7: 892 | resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} 893 | 894 | vfile@6.0.3: 895 | resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} 896 | 897 | web-namespaces@2.0.1: 898 | resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} 899 | 900 | zod@4.1.12: 901 | resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} 902 | 903 | zwitch@2.0.4: 904 | resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} 905 | 906 | snapshots: 907 | 908 | '@mapbox/hast-util-table-cell-style@0.2.1': 909 | dependencies: 910 | unist-util-visit: 1.4.1 911 | 912 | '@rollup/plugin-typescript@12.1.4(rollup@4.52.4)(tslib@2.8.1)(typescript@5.9.3)': 913 | dependencies: 914 | '@rollup/pluginutils': 5.3.0(rollup@4.52.4) 915 | resolve: 1.22.10 916 | typescript: 5.9.3 917 | optionalDependencies: 918 | rollup: 4.52.4 919 | tslib: 2.8.1 920 | 921 | '@rollup/pluginutils@5.3.0(rollup@4.52.4)': 922 | dependencies: 923 | '@types/estree': 1.0.8 924 | estree-walker: 2.0.2 925 | picomatch: 4.0.3 926 | optionalDependencies: 927 | rollup: 4.52.4 928 | 929 | '@rollup/rollup-android-arm-eabi@4.52.4': 930 | optional: true 931 | 932 | '@rollup/rollup-android-arm64@4.52.4': 933 | optional: true 934 | 935 | '@rollup/rollup-darwin-arm64@4.52.4': 936 | optional: true 937 | 938 | '@rollup/rollup-darwin-x64@4.52.4': 939 | optional: true 940 | 941 | '@rollup/rollup-freebsd-arm64@4.52.4': 942 | optional: true 943 | 944 | '@rollup/rollup-freebsd-x64@4.52.4': 945 | optional: true 946 | 947 | '@rollup/rollup-linux-arm-gnueabihf@4.52.4': 948 | optional: true 949 | 950 | '@rollup/rollup-linux-arm-musleabihf@4.52.4': 951 | optional: true 952 | 953 | '@rollup/rollup-linux-arm64-gnu@4.52.4': 954 | optional: true 955 | 956 | '@rollup/rollup-linux-arm64-musl@4.52.4': 957 | optional: true 958 | 959 | '@rollup/rollup-linux-loong64-gnu@4.52.4': 960 | optional: true 961 | 962 | '@rollup/rollup-linux-ppc64-gnu@4.52.4': 963 | optional: true 964 | 965 | '@rollup/rollup-linux-riscv64-gnu@4.52.4': 966 | optional: true 967 | 968 | '@rollup/rollup-linux-riscv64-musl@4.52.4': 969 | optional: true 970 | 971 | '@rollup/rollup-linux-s390x-gnu@4.52.4': 972 | optional: true 973 | 974 | '@rollup/rollup-linux-x64-gnu@4.52.4': 975 | optional: true 976 | 977 | '@rollup/rollup-linux-x64-musl@4.52.4': 978 | optional: true 979 | 980 | '@rollup/rollup-openharmony-arm64@4.52.4': 981 | optional: true 982 | 983 | '@rollup/rollup-win32-arm64-msvc@4.52.4': 984 | optional: true 985 | 986 | '@rollup/rollup-win32-ia32-msvc@4.52.4': 987 | optional: true 988 | 989 | '@rollup/rollup-win32-x64-gnu@4.52.4': 990 | optional: true 991 | 992 | '@rollup/rollup-win32-x64-msvc@4.52.4': 993 | optional: true 994 | 995 | '@selderee/plugin-htmlparser2@0.11.0': 996 | dependencies: 997 | domhandler: 5.0.3 998 | selderee: 0.11.0 999 | 1000 | '@types/debug@4.1.12': 1001 | dependencies: 1002 | '@types/ms': 2.1.0 1003 | 1004 | '@types/estree@1.0.8': {} 1005 | 1006 | '@types/extend@3.0.4': {} 1007 | 1008 | '@types/hast@2.3.10': 1009 | dependencies: 1010 | '@types/unist': 2.0.11 1011 | 1012 | '@types/html-to-text@9.0.4': {} 1013 | 1014 | '@types/lodash@4.17.20': {} 1015 | 1016 | '@types/luxon@3.7.1': {} 1017 | 1018 | '@types/mdast@3.0.15': 1019 | dependencies: 1020 | '@types/unist': 2.0.11 1021 | 1022 | '@types/mdast@4.0.4': 1023 | dependencies: 1024 | '@types/unist': 3.0.3 1025 | 1026 | '@types/ms@2.1.0': {} 1027 | 1028 | '@types/parse5@6.0.3': {} 1029 | 1030 | '@types/prop-types@15.7.15': {} 1031 | 1032 | '@types/react@18.3.26': 1033 | dependencies: 1034 | '@types/prop-types': 15.7.15 1035 | csstype: 3.1.3 1036 | 1037 | '@types/unist@2.0.11': {} 1038 | 1039 | '@types/unist@3.0.3': {} 1040 | 1041 | bail@2.0.2: {} 1042 | 1043 | ccount@2.0.1: {} 1044 | 1045 | character-entities-html4@2.1.0: {} 1046 | 1047 | character-entities-legacy@3.0.0: {} 1048 | 1049 | character-entities@2.0.2: {} 1050 | 1051 | comma-separated-tokens@2.0.3: {} 1052 | 1053 | csstype@3.1.3: {} 1054 | 1055 | debug@4.4.3: 1056 | dependencies: 1057 | ms: 2.1.3 1058 | 1059 | decode-named-character-reference@1.2.0: 1060 | dependencies: 1061 | character-entities: 2.0.2 1062 | 1063 | deepmerge@4.3.1: {} 1064 | 1065 | dequal@2.0.3: {} 1066 | 1067 | devlop@1.1.0: 1068 | dependencies: 1069 | dequal: 2.0.3 1070 | 1071 | diff@5.2.0: {} 1072 | 1073 | dom-serializer@2.0.0: 1074 | dependencies: 1075 | domelementtype: 2.3.0 1076 | domhandler: 5.0.3 1077 | entities: 4.5.0 1078 | 1079 | domelementtype@2.3.0: {} 1080 | 1081 | domhandler@5.0.3: 1082 | dependencies: 1083 | domelementtype: 2.3.0 1084 | 1085 | domutils@3.2.2: 1086 | dependencies: 1087 | dom-serializer: 2.0.0 1088 | domelementtype: 2.3.0 1089 | domhandler: 5.0.3 1090 | 1091 | entities@4.5.0: {} 1092 | 1093 | escape-string-regexp@5.0.0: {} 1094 | 1095 | estree-walker@2.0.2: {} 1096 | 1097 | extend@3.0.2: {} 1098 | 1099 | fsevents@2.3.3: 1100 | optional: true 1101 | 1102 | function-bind@1.1.2: {} 1103 | 1104 | hasown@2.0.2: 1105 | dependencies: 1106 | function-bind: 1.1.2 1107 | 1108 | hast-to-hyperscript@10.0.3: 1109 | dependencies: 1110 | '@types/unist': 2.0.11 1111 | comma-separated-tokens: 2.0.3 1112 | property-information: 6.5.0 1113 | space-separated-tokens: 2.0.2 1114 | style-to-object: 0.4.4 1115 | web-namespaces: 2.0.1 1116 | 1117 | hast-util-embedded@2.0.1: 1118 | dependencies: 1119 | hast-util-is-element: 2.1.3 1120 | 1121 | hast-util-from-parse5@7.1.2: 1122 | dependencies: 1123 | '@types/hast': 2.3.10 1124 | '@types/unist': 2.0.11 1125 | hastscript: 7.2.0 1126 | property-information: 6.5.0 1127 | vfile: 5.3.7 1128 | vfile-location: 4.1.0 1129 | web-namespaces: 2.0.1 1130 | 1131 | hast-util-has-property@2.0.1: {} 1132 | 1133 | hast-util-is-body-ok-link@2.0.0: 1134 | dependencies: 1135 | '@types/hast': 2.3.10 1136 | hast-util-has-property: 2.0.1 1137 | hast-util-is-element: 2.1.3 1138 | 1139 | hast-util-is-element@2.1.3: 1140 | dependencies: 1141 | '@types/hast': 2.3.10 1142 | '@types/unist': 2.0.11 1143 | 1144 | hast-util-parse-selector@3.1.1: 1145 | dependencies: 1146 | '@types/hast': 2.3.10 1147 | 1148 | hast-util-phrasing@2.0.2: 1149 | dependencies: 1150 | '@types/hast': 2.3.10 1151 | hast-util-embedded: 2.0.1 1152 | hast-util-has-property: 2.0.1 1153 | hast-util-is-body-ok-link: 2.0.0 1154 | hast-util-is-element: 2.1.3 1155 | 1156 | hast-util-raw@7.2.3: 1157 | dependencies: 1158 | '@types/hast': 2.3.10 1159 | '@types/parse5': 6.0.3 1160 | hast-util-from-parse5: 7.1.2 1161 | hast-util-to-parse5: 7.1.0 1162 | html-void-elements: 2.0.1 1163 | parse5: 6.0.1 1164 | unist-util-position: 4.0.4 1165 | unist-util-visit: 4.1.2 1166 | vfile: 5.3.7 1167 | web-namespaces: 2.0.1 1168 | zwitch: 2.0.4 1169 | 1170 | hast-util-sanitize@4.1.0: 1171 | dependencies: 1172 | '@types/hast': 2.3.10 1173 | 1174 | hast-util-to-html@8.0.4: 1175 | dependencies: 1176 | '@types/hast': 2.3.10 1177 | '@types/unist': 2.0.11 1178 | ccount: 2.0.1 1179 | comma-separated-tokens: 2.0.3 1180 | hast-util-raw: 7.2.3 1181 | hast-util-whitespace: 2.0.1 1182 | html-void-elements: 2.0.1 1183 | property-information: 6.5.0 1184 | space-separated-tokens: 2.0.2 1185 | stringify-entities: 4.0.4 1186 | zwitch: 2.0.4 1187 | 1188 | hast-util-to-mdast@8.4.1: 1189 | dependencies: 1190 | '@types/extend': 3.0.4 1191 | '@types/hast': 2.3.10 1192 | '@types/mdast': 3.0.15 1193 | '@types/unist': 2.0.11 1194 | extend: 3.0.2 1195 | hast-util-has-property: 2.0.1 1196 | hast-util-is-element: 2.1.3 1197 | hast-util-phrasing: 2.0.2 1198 | hast-util-to-text: 3.1.2 1199 | mdast-util-phrasing: 3.0.1 1200 | mdast-util-to-string: 3.2.0 1201 | rehype-minify-whitespace: 5.0.1 1202 | trim-trailing-lines: 2.1.0 1203 | unist-util-is: 5.2.1 1204 | unist-util-visit: 4.1.2 1205 | 1206 | hast-util-to-parse5@7.1.0: 1207 | dependencies: 1208 | '@types/hast': 2.3.10 1209 | comma-separated-tokens: 2.0.3 1210 | property-information: 6.5.0 1211 | space-separated-tokens: 2.0.2 1212 | web-namespaces: 2.0.1 1213 | zwitch: 2.0.4 1214 | 1215 | hast-util-to-text@3.1.2: 1216 | dependencies: 1217 | '@types/hast': 2.3.10 1218 | '@types/unist': 2.0.11 1219 | hast-util-is-element: 2.1.3 1220 | unist-util-find-after: 4.0.1 1221 | 1222 | hast-util-whitespace@2.0.1: {} 1223 | 1224 | hastscript@7.2.0: 1225 | dependencies: 1226 | '@types/hast': 2.3.10 1227 | comma-separated-tokens: 2.0.3 1228 | hast-util-parse-selector: 3.1.1 1229 | property-information: 6.5.0 1230 | space-separated-tokens: 2.0.2 1231 | 1232 | html-to-text@9.0.5: 1233 | dependencies: 1234 | '@selderee/plugin-htmlparser2': 0.11.0 1235 | deepmerge: 4.3.1 1236 | dom-serializer: 2.0.0 1237 | htmlparser2: 8.0.2 1238 | selderee: 0.11.0 1239 | 1240 | html-void-elements@2.0.1: {} 1241 | 1242 | htmlparser2@8.0.2: 1243 | dependencies: 1244 | domelementtype: 2.3.0 1245 | domhandler: 5.0.3 1246 | domutils: 3.2.2 1247 | entities: 4.5.0 1248 | 1249 | inline-style-parser@0.1.1: {} 1250 | 1251 | inline-style-parser@0.2.4: {} 1252 | 1253 | is-absolute-url@4.0.1: {} 1254 | 1255 | is-buffer@2.0.5: {} 1256 | 1257 | is-core-module@2.16.1: 1258 | dependencies: 1259 | hasown: 2.0.2 1260 | 1261 | is-plain-obj@4.1.0: {} 1262 | 1263 | js-tokens@4.0.0: {} 1264 | 1265 | kleur@4.1.5: {} 1266 | 1267 | leac@0.6.0: {} 1268 | 1269 | lodash@4.17.21: {} 1270 | 1271 | longest-streak@3.1.0: {} 1272 | 1273 | loose-envify@1.4.0: 1274 | dependencies: 1275 | js-tokens: 4.0.0 1276 | 1277 | luxon@3.7.2: {} 1278 | 1279 | markdown-table@3.0.4: {} 1280 | 1281 | mdast-util-definitions@5.1.2: 1282 | dependencies: 1283 | '@types/mdast': 3.0.15 1284 | '@types/unist': 2.0.11 1285 | unist-util-visit: 4.1.2 1286 | 1287 | mdast-util-find-and-replace@2.2.2: 1288 | dependencies: 1289 | '@types/mdast': 3.0.15 1290 | escape-string-regexp: 5.0.0 1291 | unist-util-is: 5.2.1 1292 | unist-util-visit-parents: 5.1.3 1293 | 1294 | mdast-util-from-markdown@1.3.1: 1295 | dependencies: 1296 | '@types/mdast': 3.0.15 1297 | '@types/unist': 2.0.11 1298 | decode-named-character-reference: 1.2.0 1299 | mdast-util-to-string: 3.2.0 1300 | micromark: 3.2.0 1301 | micromark-util-decode-numeric-character-reference: 1.1.0 1302 | micromark-util-decode-string: 1.1.0 1303 | micromark-util-normalize-identifier: 1.1.0 1304 | micromark-util-symbol: 1.1.0 1305 | micromark-util-types: 1.1.0 1306 | unist-util-stringify-position: 3.0.3 1307 | uvu: 0.5.6 1308 | transitivePeerDependencies: 1309 | - supports-color 1310 | 1311 | mdast-util-from-markdown@2.0.2: 1312 | dependencies: 1313 | '@types/mdast': 4.0.4 1314 | '@types/unist': 3.0.3 1315 | decode-named-character-reference: 1.2.0 1316 | devlop: 1.1.0 1317 | mdast-util-to-string: 4.0.0 1318 | micromark: 4.0.2 1319 | micromark-util-decode-numeric-character-reference: 2.0.2 1320 | micromark-util-decode-string: 2.0.1 1321 | micromark-util-normalize-identifier: 2.0.1 1322 | micromark-util-symbol: 2.0.1 1323 | micromark-util-types: 2.0.2 1324 | unist-util-stringify-position: 4.0.0 1325 | transitivePeerDependencies: 1326 | - supports-color 1327 | 1328 | mdast-util-gfm-autolink-literal@1.0.3: 1329 | dependencies: 1330 | '@types/mdast': 3.0.15 1331 | ccount: 2.0.1 1332 | mdast-util-find-and-replace: 2.2.2 1333 | micromark-util-character: 1.2.0 1334 | 1335 | mdast-util-gfm-footnote@1.0.2: 1336 | dependencies: 1337 | '@types/mdast': 3.0.15 1338 | mdast-util-to-markdown: 1.5.0 1339 | micromark-util-normalize-identifier: 1.1.0 1340 | 1341 | mdast-util-gfm-strikethrough@1.0.3: 1342 | dependencies: 1343 | '@types/mdast': 3.0.15 1344 | mdast-util-to-markdown: 1.5.0 1345 | 1346 | mdast-util-gfm-table@1.0.7: 1347 | dependencies: 1348 | '@types/mdast': 3.0.15 1349 | markdown-table: 3.0.4 1350 | mdast-util-from-markdown: 1.3.1 1351 | mdast-util-to-markdown: 1.5.0 1352 | transitivePeerDependencies: 1353 | - supports-color 1354 | 1355 | mdast-util-gfm-task-list-item@1.0.2: 1356 | dependencies: 1357 | '@types/mdast': 3.0.15 1358 | mdast-util-to-markdown: 1.5.0 1359 | 1360 | mdast-util-gfm@2.0.2: 1361 | dependencies: 1362 | mdast-util-from-markdown: 1.3.1 1363 | mdast-util-gfm-autolink-literal: 1.0.3 1364 | mdast-util-gfm-footnote: 1.0.2 1365 | mdast-util-gfm-strikethrough: 1.0.3 1366 | mdast-util-gfm-table: 1.0.7 1367 | mdast-util-gfm-task-list-item: 1.0.2 1368 | mdast-util-to-markdown: 1.5.0 1369 | transitivePeerDependencies: 1370 | - supports-color 1371 | 1372 | mdast-util-newline-to-break@1.0.0: 1373 | dependencies: 1374 | '@types/mdast': 3.0.15 1375 | mdast-util-find-and-replace: 2.2.2 1376 | 1377 | mdast-util-phrasing@3.0.1: 1378 | dependencies: 1379 | '@types/mdast': 3.0.15 1380 | unist-util-is: 5.2.1 1381 | 1382 | mdast-util-phrasing@4.1.0: 1383 | dependencies: 1384 | '@types/mdast': 4.0.4 1385 | unist-util-is: 6.0.1 1386 | 1387 | mdast-util-to-hast@12.3.0: 1388 | dependencies: 1389 | '@types/hast': 2.3.10 1390 | '@types/mdast': 3.0.15 1391 | mdast-util-definitions: 5.1.2 1392 | micromark-util-sanitize-uri: 1.2.0 1393 | trim-lines: 3.0.1 1394 | unist-util-generated: 2.0.1 1395 | unist-util-position: 4.0.4 1396 | unist-util-visit: 4.1.2 1397 | 1398 | mdast-util-to-markdown@1.5.0: 1399 | dependencies: 1400 | '@types/mdast': 3.0.15 1401 | '@types/unist': 2.0.11 1402 | longest-streak: 3.1.0 1403 | mdast-util-phrasing: 3.0.1 1404 | mdast-util-to-string: 3.2.0 1405 | micromark-util-decode-string: 1.1.0 1406 | unist-util-visit: 4.1.2 1407 | zwitch: 2.0.4 1408 | 1409 | mdast-util-to-markdown@2.1.2: 1410 | dependencies: 1411 | '@types/mdast': 4.0.4 1412 | '@types/unist': 3.0.3 1413 | longest-streak: 3.1.0 1414 | mdast-util-phrasing: 4.1.0 1415 | mdast-util-to-string: 4.0.0 1416 | micromark-util-classify-character: 2.0.1 1417 | micromark-util-decode-string: 2.0.1 1418 | unist-util-visit: 5.0.0 1419 | zwitch: 2.0.4 1420 | 1421 | mdast-util-to-string@3.2.0: 1422 | dependencies: 1423 | '@types/mdast': 3.0.15 1424 | 1425 | mdast-util-to-string@4.0.0: 1426 | dependencies: 1427 | '@types/mdast': 4.0.4 1428 | 1429 | micromark-core-commonmark@1.1.0: 1430 | dependencies: 1431 | decode-named-character-reference: 1.2.0 1432 | micromark-factory-destination: 1.1.0 1433 | micromark-factory-label: 1.1.0 1434 | micromark-factory-space: 1.1.0 1435 | micromark-factory-title: 1.1.0 1436 | micromark-factory-whitespace: 1.1.0 1437 | micromark-util-character: 1.2.0 1438 | micromark-util-chunked: 1.1.0 1439 | micromark-util-classify-character: 1.1.0 1440 | micromark-util-html-tag-name: 1.2.0 1441 | micromark-util-normalize-identifier: 1.1.0 1442 | micromark-util-resolve-all: 1.1.0 1443 | micromark-util-subtokenize: 1.1.0 1444 | micromark-util-symbol: 1.1.0 1445 | micromark-util-types: 1.1.0 1446 | uvu: 0.5.6 1447 | 1448 | micromark-core-commonmark@2.0.3: 1449 | dependencies: 1450 | decode-named-character-reference: 1.2.0 1451 | devlop: 1.1.0 1452 | micromark-factory-destination: 2.0.1 1453 | micromark-factory-label: 2.0.1 1454 | micromark-factory-space: 2.0.1 1455 | micromark-factory-title: 2.0.1 1456 | micromark-factory-whitespace: 2.0.1 1457 | micromark-util-character: 2.1.1 1458 | micromark-util-chunked: 2.0.1 1459 | micromark-util-classify-character: 2.0.1 1460 | micromark-util-html-tag-name: 2.0.1 1461 | micromark-util-normalize-identifier: 2.0.1 1462 | micromark-util-resolve-all: 2.0.1 1463 | micromark-util-subtokenize: 2.1.0 1464 | micromark-util-symbol: 2.0.1 1465 | micromark-util-types: 2.0.2 1466 | 1467 | micromark-extension-gfm-autolink-literal@1.0.5: 1468 | dependencies: 1469 | micromark-util-character: 1.2.0 1470 | micromark-util-sanitize-uri: 1.2.0 1471 | micromark-util-symbol: 1.1.0 1472 | micromark-util-types: 1.1.0 1473 | 1474 | micromark-extension-gfm-footnote@1.1.2: 1475 | dependencies: 1476 | micromark-core-commonmark: 1.1.0 1477 | micromark-factory-space: 1.1.0 1478 | micromark-util-character: 1.2.0 1479 | micromark-util-normalize-identifier: 1.1.0 1480 | micromark-util-sanitize-uri: 1.2.0 1481 | micromark-util-symbol: 1.1.0 1482 | micromark-util-types: 1.1.0 1483 | uvu: 0.5.6 1484 | 1485 | micromark-extension-gfm-strikethrough@1.0.7: 1486 | dependencies: 1487 | micromark-util-chunked: 1.1.0 1488 | micromark-util-classify-character: 1.1.0 1489 | micromark-util-resolve-all: 1.1.0 1490 | micromark-util-symbol: 1.1.0 1491 | micromark-util-types: 1.1.0 1492 | uvu: 0.5.6 1493 | 1494 | micromark-extension-gfm-table@1.0.7: 1495 | dependencies: 1496 | micromark-factory-space: 1.1.0 1497 | micromark-util-character: 1.2.0 1498 | micromark-util-symbol: 1.1.0 1499 | micromark-util-types: 1.1.0 1500 | uvu: 0.5.6 1501 | 1502 | micromark-extension-gfm-tagfilter@1.0.2: 1503 | dependencies: 1504 | micromark-util-types: 1.1.0 1505 | 1506 | micromark-extension-gfm-task-list-item@1.0.5: 1507 | dependencies: 1508 | micromark-factory-space: 1.1.0 1509 | micromark-util-character: 1.2.0 1510 | micromark-util-symbol: 1.1.0 1511 | micromark-util-types: 1.1.0 1512 | uvu: 0.5.6 1513 | 1514 | micromark-extension-gfm@2.0.3: 1515 | dependencies: 1516 | micromark-extension-gfm-autolink-literal: 1.0.5 1517 | micromark-extension-gfm-footnote: 1.1.2 1518 | micromark-extension-gfm-strikethrough: 1.0.7 1519 | micromark-extension-gfm-table: 1.0.7 1520 | micromark-extension-gfm-tagfilter: 1.0.2 1521 | micromark-extension-gfm-task-list-item: 1.0.5 1522 | micromark-util-combine-extensions: 1.1.0 1523 | micromark-util-types: 1.1.0 1524 | 1525 | micromark-factory-destination@1.1.0: 1526 | dependencies: 1527 | micromark-util-character: 1.2.0 1528 | micromark-util-symbol: 1.1.0 1529 | micromark-util-types: 1.1.0 1530 | 1531 | micromark-factory-destination@2.0.1: 1532 | dependencies: 1533 | micromark-util-character: 2.1.1 1534 | micromark-util-symbol: 2.0.1 1535 | micromark-util-types: 2.0.2 1536 | 1537 | micromark-factory-label@1.1.0: 1538 | dependencies: 1539 | micromark-util-character: 1.2.0 1540 | micromark-util-symbol: 1.1.0 1541 | micromark-util-types: 1.1.0 1542 | uvu: 0.5.6 1543 | 1544 | micromark-factory-label@2.0.1: 1545 | dependencies: 1546 | devlop: 1.1.0 1547 | micromark-util-character: 2.1.1 1548 | micromark-util-symbol: 2.0.1 1549 | micromark-util-types: 2.0.2 1550 | 1551 | micromark-factory-space@1.1.0: 1552 | dependencies: 1553 | micromark-util-character: 1.2.0 1554 | micromark-util-types: 1.1.0 1555 | 1556 | micromark-factory-space@2.0.1: 1557 | dependencies: 1558 | micromark-util-character: 2.1.1 1559 | micromark-util-types: 2.0.2 1560 | 1561 | micromark-factory-title@1.1.0: 1562 | dependencies: 1563 | micromark-factory-space: 1.1.0 1564 | micromark-util-character: 1.2.0 1565 | micromark-util-symbol: 1.1.0 1566 | micromark-util-types: 1.1.0 1567 | 1568 | micromark-factory-title@2.0.1: 1569 | dependencies: 1570 | micromark-factory-space: 2.0.1 1571 | micromark-util-character: 2.1.1 1572 | micromark-util-symbol: 2.0.1 1573 | micromark-util-types: 2.0.2 1574 | 1575 | micromark-factory-whitespace@1.1.0: 1576 | dependencies: 1577 | micromark-factory-space: 1.1.0 1578 | micromark-util-character: 1.2.0 1579 | micromark-util-symbol: 1.1.0 1580 | micromark-util-types: 1.1.0 1581 | 1582 | micromark-factory-whitespace@2.0.1: 1583 | dependencies: 1584 | micromark-factory-space: 2.0.1 1585 | micromark-util-character: 2.1.1 1586 | micromark-util-symbol: 2.0.1 1587 | micromark-util-types: 2.0.2 1588 | 1589 | micromark-util-character@1.2.0: 1590 | dependencies: 1591 | micromark-util-symbol: 1.1.0 1592 | micromark-util-types: 1.1.0 1593 | 1594 | micromark-util-character@2.1.1: 1595 | dependencies: 1596 | micromark-util-symbol: 2.0.1 1597 | micromark-util-types: 2.0.2 1598 | 1599 | micromark-util-chunked@1.1.0: 1600 | dependencies: 1601 | micromark-util-symbol: 1.1.0 1602 | 1603 | micromark-util-chunked@2.0.1: 1604 | dependencies: 1605 | micromark-util-symbol: 2.0.1 1606 | 1607 | micromark-util-classify-character@1.1.0: 1608 | dependencies: 1609 | micromark-util-character: 1.2.0 1610 | micromark-util-symbol: 1.1.0 1611 | micromark-util-types: 1.1.0 1612 | 1613 | micromark-util-classify-character@2.0.1: 1614 | dependencies: 1615 | micromark-util-character: 2.1.1 1616 | micromark-util-symbol: 2.0.1 1617 | micromark-util-types: 2.0.2 1618 | 1619 | micromark-util-combine-extensions@1.1.0: 1620 | dependencies: 1621 | micromark-util-chunked: 1.1.0 1622 | micromark-util-types: 1.1.0 1623 | 1624 | micromark-util-combine-extensions@2.0.1: 1625 | dependencies: 1626 | micromark-util-chunked: 2.0.1 1627 | micromark-util-types: 2.0.2 1628 | 1629 | micromark-util-decode-numeric-character-reference@1.1.0: 1630 | dependencies: 1631 | micromark-util-symbol: 1.1.0 1632 | 1633 | micromark-util-decode-numeric-character-reference@2.0.2: 1634 | dependencies: 1635 | micromark-util-symbol: 2.0.1 1636 | 1637 | micromark-util-decode-string@1.1.0: 1638 | dependencies: 1639 | decode-named-character-reference: 1.2.0 1640 | micromark-util-character: 1.2.0 1641 | micromark-util-decode-numeric-character-reference: 1.1.0 1642 | micromark-util-symbol: 1.1.0 1643 | 1644 | micromark-util-decode-string@2.0.1: 1645 | dependencies: 1646 | decode-named-character-reference: 1.2.0 1647 | micromark-util-character: 2.1.1 1648 | micromark-util-decode-numeric-character-reference: 2.0.2 1649 | micromark-util-symbol: 2.0.1 1650 | 1651 | micromark-util-encode@1.1.0: {} 1652 | 1653 | micromark-util-encode@2.0.1: {} 1654 | 1655 | micromark-util-html-tag-name@1.2.0: {} 1656 | 1657 | micromark-util-html-tag-name@2.0.1: {} 1658 | 1659 | micromark-util-normalize-identifier@1.1.0: 1660 | dependencies: 1661 | micromark-util-symbol: 1.1.0 1662 | 1663 | micromark-util-normalize-identifier@2.0.1: 1664 | dependencies: 1665 | micromark-util-symbol: 2.0.1 1666 | 1667 | micromark-util-resolve-all@1.1.0: 1668 | dependencies: 1669 | micromark-util-types: 1.1.0 1670 | 1671 | micromark-util-resolve-all@2.0.1: 1672 | dependencies: 1673 | micromark-util-types: 2.0.2 1674 | 1675 | micromark-util-sanitize-uri@1.2.0: 1676 | dependencies: 1677 | micromark-util-character: 1.2.0 1678 | micromark-util-encode: 1.1.0 1679 | micromark-util-symbol: 1.1.0 1680 | 1681 | micromark-util-sanitize-uri@2.0.1: 1682 | dependencies: 1683 | micromark-util-character: 2.1.1 1684 | micromark-util-encode: 2.0.1 1685 | micromark-util-symbol: 2.0.1 1686 | 1687 | micromark-util-subtokenize@1.1.0: 1688 | dependencies: 1689 | micromark-util-chunked: 1.1.0 1690 | micromark-util-symbol: 1.1.0 1691 | micromark-util-types: 1.1.0 1692 | uvu: 0.5.6 1693 | 1694 | micromark-util-subtokenize@2.1.0: 1695 | dependencies: 1696 | devlop: 1.1.0 1697 | micromark-util-chunked: 2.0.1 1698 | micromark-util-symbol: 2.0.1 1699 | micromark-util-types: 2.0.2 1700 | 1701 | micromark-util-symbol@1.1.0: {} 1702 | 1703 | micromark-util-symbol@2.0.1: {} 1704 | 1705 | micromark-util-types@1.1.0: {} 1706 | 1707 | micromark-util-types@2.0.2: {} 1708 | 1709 | micromark@3.2.0: 1710 | dependencies: 1711 | '@types/debug': 4.1.12 1712 | debug: 4.4.3 1713 | decode-named-character-reference: 1.2.0 1714 | micromark-core-commonmark: 1.1.0 1715 | micromark-factory-space: 1.1.0 1716 | micromark-util-character: 1.2.0 1717 | micromark-util-chunked: 1.1.0 1718 | micromark-util-combine-extensions: 1.1.0 1719 | micromark-util-decode-numeric-character-reference: 1.1.0 1720 | micromark-util-encode: 1.1.0 1721 | micromark-util-normalize-identifier: 1.1.0 1722 | micromark-util-resolve-all: 1.1.0 1723 | micromark-util-sanitize-uri: 1.2.0 1724 | micromark-util-subtokenize: 1.1.0 1725 | micromark-util-symbol: 1.1.0 1726 | micromark-util-types: 1.1.0 1727 | uvu: 0.5.6 1728 | transitivePeerDependencies: 1729 | - supports-color 1730 | 1731 | micromark@4.0.2: 1732 | dependencies: 1733 | '@types/debug': 4.1.12 1734 | debug: 4.4.3 1735 | decode-named-character-reference: 1.2.0 1736 | devlop: 1.1.0 1737 | micromark-core-commonmark: 2.0.3 1738 | micromark-factory-space: 2.0.1 1739 | micromark-util-character: 2.1.1 1740 | micromark-util-chunked: 2.0.1 1741 | micromark-util-combine-extensions: 2.0.1 1742 | micromark-util-decode-numeric-character-reference: 2.0.2 1743 | micromark-util-encode: 2.0.1 1744 | micromark-util-normalize-identifier: 2.0.1 1745 | micromark-util-resolve-all: 2.0.1 1746 | micromark-util-sanitize-uri: 2.0.1 1747 | micromark-util-subtokenize: 2.1.0 1748 | micromark-util-symbol: 2.0.1 1749 | micromark-util-types: 2.0.2 1750 | transitivePeerDependencies: 1751 | - supports-color 1752 | 1753 | mri@1.2.0: {} 1754 | 1755 | ms@2.1.3: {} 1756 | 1757 | parse5@6.0.1: {} 1758 | 1759 | parseley@0.12.1: 1760 | dependencies: 1761 | leac: 0.6.0 1762 | peberminta: 0.9.0 1763 | 1764 | path-parse@1.0.7: {} 1765 | 1766 | peberminta@0.9.0: {} 1767 | 1768 | picomatch@4.0.3: {} 1769 | 1770 | property-information@6.5.0: {} 1771 | 1772 | react@18.3.1: 1773 | dependencies: 1774 | loose-envify: 1.4.0 1775 | 1776 | rehype-external-links@2.1.0: 1777 | dependencies: 1778 | '@types/hast': 2.3.10 1779 | extend: 3.0.2 1780 | hast-util-is-element: 2.1.3 1781 | is-absolute-url: 4.0.1 1782 | space-separated-tokens: 2.0.2 1783 | unified: 10.1.2 1784 | unist-util-visit: 4.1.2 1785 | 1786 | rehype-minify-whitespace@5.0.1: 1787 | dependencies: 1788 | '@types/hast': 2.3.10 1789 | hast-util-embedded: 2.0.1 1790 | hast-util-is-element: 2.1.3 1791 | hast-util-whitespace: 2.0.1 1792 | unified: 10.1.2 1793 | unist-util-is: 5.2.1 1794 | 1795 | rehype-raw@6.1.1: 1796 | dependencies: 1797 | '@types/hast': 2.3.10 1798 | hast-util-raw: 7.2.3 1799 | unified: 10.1.2 1800 | 1801 | rehype-react@7.2.0(@types/react@18.3.26): 1802 | dependencies: 1803 | '@mapbox/hast-util-table-cell-style': 0.2.1 1804 | '@types/hast': 2.3.10 1805 | '@types/react': 18.3.26 1806 | hast-to-hyperscript: 10.0.3 1807 | hast-util-whitespace: 2.0.1 1808 | unified: 10.1.2 1809 | 1810 | rehype-remark@9.1.2: 1811 | dependencies: 1812 | '@types/hast': 2.3.10 1813 | '@types/mdast': 3.0.15 1814 | hast-util-to-mdast: 8.4.1 1815 | unified: 10.1.2 1816 | 1817 | rehype-sanitize@5.0.1: 1818 | dependencies: 1819 | '@types/hast': 2.3.10 1820 | hast-util-sanitize: 4.1.0 1821 | unified: 10.1.2 1822 | 1823 | rehype-stringify@9.0.4: 1824 | dependencies: 1825 | '@types/hast': 2.3.10 1826 | hast-util-to-html: 8.0.4 1827 | unified: 10.1.2 1828 | 1829 | remark-breaks@3.0.3: 1830 | dependencies: 1831 | '@types/mdast': 3.0.15 1832 | mdast-util-newline-to-break: 1.0.0 1833 | unified: 10.1.2 1834 | 1835 | remark-gfm@3.0.1: 1836 | dependencies: 1837 | '@types/mdast': 3.0.15 1838 | mdast-util-gfm: 2.0.2 1839 | micromark-extension-gfm: 2.0.3 1840 | unified: 10.1.2 1841 | transitivePeerDependencies: 1842 | - supports-color 1843 | 1844 | remark-parse@10.0.2: 1845 | dependencies: 1846 | '@types/mdast': 3.0.15 1847 | mdast-util-from-markdown: 1.3.1 1848 | unified: 10.1.2 1849 | transitivePeerDependencies: 1850 | - supports-color 1851 | 1852 | remark-parse@11.0.0: 1853 | dependencies: 1854 | '@types/mdast': 4.0.4 1855 | mdast-util-from-markdown: 2.0.2 1856 | micromark-util-types: 2.0.2 1857 | unified: 11.0.5 1858 | transitivePeerDependencies: 1859 | - supports-color 1860 | 1861 | remark-rehype@10.1.0: 1862 | dependencies: 1863 | '@types/hast': 2.3.10 1864 | '@types/mdast': 3.0.15 1865 | mdast-util-to-hast: 12.3.0 1866 | unified: 10.1.2 1867 | 1868 | remark-stringify@10.0.3: 1869 | dependencies: 1870 | '@types/mdast': 3.0.15 1871 | mdast-util-to-markdown: 1.5.0 1872 | unified: 10.1.2 1873 | 1874 | remark-stringify@11.0.0: 1875 | dependencies: 1876 | '@types/mdast': 4.0.4 1877 | mdast-util-to-markdown: 2.1.2 1878 | unified: 11.0.5 1879 | 1880 | remark@15.0.1: 1881 | dependencies: 1882 | '@types/mdast': 4.0.4 1883 | remark-parse: 11.0.0 1884 | remark-stringify: 11.0.0 1885 | unified: 11.0.5 1886 | transitivePeerDependencies: 1887 | - supports-color 1888 | 1889 | resolve@1.22.10: 1890 | dependencies: 1891 | is-core-module: 2.16.1 1892 | path-parse: 1.0.7 1893 | supports-preserve-symlinks-flag: 1.0.0 1894 | 1895 | rollup@4.52.4: 1896 | dependencies: 1897 | '@types/estree': 1.0.8 1898 | optionalDependencies: 1899 | '@rollup/rollup-android-arm-eabi': 4.52.4 1900 | '@rollup/rollup-android-arm64': 4.52.4 1901 | '@rollup/rollup-darwin-arm64': 4.52.4 1902 | '@rollup/rollup-darwin-x64': 4.52.4 1903 | '@rollup/rollup-freebsd-arm64': 4.52.4 1904 | '@rollup/rollup-freebsd-x64': 4.52.4 1905 | '@rollup/rollup-linux-arm-gnueabihf': 4.52.4 1906 | '@rollup/rollup-linux-arm-musleabihf': 4.52.4 1907 | '@rollup/rollup-linux-arm64-gnu': 4.52.4 1908 | '@rollup/rollup-linux-arm64-musl': 4.52.4 1909 | '@rollup/rollup-linux-loong64-gnu': 4.52.4 1910 | '@rollup/rollup-linux-ppc64-gnu': 4.52.4 1911 | '@rollup/rollup-linux-riscv64-gnu': 4.52.4 1912 | '@rollup/rollup-linux-riscv64-musl': 4.52.4 1913 | '@rollup/rollup-linux-s390x-gnu': 4.52.4 1914 | '@rollup/rollup-linux-x64-gnu': 4.52.4 1915 | '@rollup/rollup-linux-x64-musl': 4.52.4 1916 | '@rollup/rollup-openharmony-arm64': 4.52.4 1917 | '@rollup/rollup-win32-arm64-msvc': 4.52.4 1918 | '@rollup/rollup-win32-ia32-msvc': 4.52.4 1919 | '@rollup/rollup-win32-x64-gnu': 4.52.4 1920 | '@rollup/rollup-win32-x64-msvc': 4.52.4 1921 | fsevents: 2.3.3 1922 | 1923 | sade@1.8.1: 1924 | dependencies: 1925 | mri: 1.2.0 1926 | 1927 | selderee@0.11.0: 1928 | dependencies: 1929 | parseley: 0.12.1 1930 | 1931 | space-separated-tokens@2.0.2: {} 1932 | 1933 | stringify-entities@4.0.4: 1934 | dependencies: 1935 | character-entities-html4: 2.1.0 1936 | character-entities-legacy: 3.0.0 1937 | 1938 | style-to-object@0.4.4: 1939 | dependencies: 1940 | inline-style-parser: 0.1.1 1941 | 1942 | style-to-object@1.0.11: 1943 | dependencies: 1944 | inline-style-parser: 0.2.4 1945 | 1946 | supports-preserve-symlinks-flag@1.0.0: {} 1947 | 1948 | trim-lines@3.0.1: {} 1949 | 1950 | trim-trailing-lines@2.1.0: {} 1951 | 1952 | trough@2.2.0: {} 1953 | 1954 | tslib@2.8.1: {} 1955 | 1956 | typescript@5.9.3: {} 1957 | 1958 | unified@10.1.2: 1959 | dependencies: 1960 | '@types/unist': 2.0.11 1961 | bail: 2.0.2 1962 | extend: 3.0.2 1963 | is-buffer: 2.0.5 1964 | is-plain-obj: 4.1.0 1965 | trough: 2.2.0 1966 | vfile: 5.3.7 1967 | 1968 | unified@11.0.5: 1969 | dependencies: 1970 | '@types/unist': 3.0.3 1971 | bail: 2.0.2 1972 | devlop: 1.1.0 1973 | extend: 3.0.2 1974 | is-plain-obj: 4.1.0 1975 | trough: 2.2.0 1976 | vfile: 6.0.3 1977 | 1978 | unist-builder@3.0.1: 1979 | dependencies: 1980 | '@types/unist': 2.0.11 1981 | 1982 | unist-util-find-after@4.0.1: 1983 | dependencies: 1984 | '@types/unist': 2.0.11 1985 | unist-util-is: 5.2.1 1986 | 1987 | unist-util-generated@2.0.1: {} 1988 | 1989 | unist-util-is@3.0.0: {} 1990 | 1991 | unist-util-is@5.2.1: 1992 | dependencies: 1993 | '@types/unist': 2.0.11 1994 | 1995 | unist-util-is@6.0.1: 1996 | dependencies: 1997 | '@types/unist': 3.0.3 1998 | 1999 | unist-util-position@4.0.4: 2000 | dependencies: 2001 | '@types/unist': 2.0.11 2002 | 2003 | unist-util-stringify-position@3.0.3: 2004 | dependencies: 2005 | '@types/unist': 2.0.11 2006 | 2007 | unist-util-stringify-position@4.0.0: 2008 | dependencies: 2009 | '@types/unist': 3.0.3 2010 | 2011 | unist-util-visit-parents@2.1.2: 2012 | dependencies: 2013 | unist-util-is: 3.0.0 2014 | 2015 | unist-util-visit-parents@5.1.3: 2016 | dependencies: 2017 | '@types/unist': 2.0.11 2018 | unist-util-is: 5.2.1 2019 | 2020 | unist-util-visit-parents@6.0.2: 2021 | dependencies: 2022 | '@types/unist': 3.0.3 2023 | unist-util-is: 6.0.1 2024 | 2025 | unist-util-visit@1.4.1: 2026 | dependencies: 2027 | unist-util-visit-parents: 2.1.2 2028 | 2029 | unist-util-visit@4.1.2: 2030 | dependencies: 2031 | '@types/unist': 2.0.11 2032 | unist-util-is: 5.2.1 2033 | unist-util-visit-parents: 5.1.3 2034 | 2035 | unist-util-visit@5.0.0: 2036 | dependencies: 2037 | '@types/unist': 3.0.3 2038 | unist-util-is: 6.0.1 2039 | unist-util-visit-parents: 6.0.2 2040 | 2041 | uvu@0.5.6: 2042 | dependencies: 2043 | dequal: 2.0.3 2044 | diff: 5.2.0 2045 | kleur: 4.1.5 2046 | sade: 1.8.1 2047 | 2048 | vfile-location@4.1.0: 2049 | dependencies: 2050 | '@types/unist': 2.0.11 2051 | vfile: 5.3.7 2052 | 2053 | vfile-message@3.1.4: 2054 | dependencies: 2055 | '@types/unist': 2.0.11 2056 | unist-util-stringify-position: 3.0.3 2057 | 2058 | vfile-message@4.0.3: 2059 | dependencies: 2060 | '@types/unist': 3.0.3 2061 | unist-util-stringify-position: 4.0.0 2062 | 2063 | vfile@5.3.7: 2064 | dependencies: 2065 | '@types/unist': 2.0.11 2066 | is-buffer: 2.0.5 2067 | unist-util-stringify-position: 3.0.3 2068 | vfile-message: 3.1.4 2069 | 2070 | vfile@6.0.3: 2071 | dependencies: 2072 | '@types/unist': 3.0.3 2073 | vfile-message: 4.0.3 2074 | 2075 | web-namespaces@2.0.1: {} 2076 | 2077 | zod@4.1.12: {} 2078 | 2079 | zwitch@2.0.4: {} 2080 | --------------------------------------------------------------------------------