diff --git a/package-lock.json b/package-lock.json index 6b8721a..5dd9450 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gallery-badmanners-xyz", - "version": "1.5.0", + "version": "1.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gallery-badmanners-xyz", - "version": "1.5.0", + "version": "1.5.1", "dependencies": { "@astrojs/check": "^0.5.10", "@astrojs/rss": "^4.0.5", @@ -15,7 +15,7 @@ "@tailwindcss/typography": "^0.5.12", "@types/sanitize-html": "^2.11.0", "astro": "^4.5.16", - "astro-pagefind": "^1.5.0", + "astro-pagefind": "^1.6.0", "github-slugger": "^2.0.0", "marked": "^12.0.1", "pagefind": "^1.1.0", @@ -1800,9 +1800,10 @@ } }, "node_modules/astro-pagefind": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/astro-pagefind/-/astro-pagefind-1.5.0.tgz", - "integrity": "sha512-CN7Afe9qW640U1qliCQMXN259Dl6VPUnl8FneLfKE7STV4HLiif4PbNZejylC6yXYyP6uyNVDHNOfJfBBW5h6A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/astro-pagefind/-/astro-pagefind-1.6.0.tgz", + "integrity": "sha512-U/WuE0ktkZkoFJf6yopWO4DjIJ3+wrnopE2L3kUYiyqNTJpqmp13bFLR8gir6B+KzQ5dsXQtJZYTQtKJg1FxIA==", + "license": "MIT", "dependencies": { "@pagefind/default-ui": "^1.0.3", "pagefind": "^1.0.3", diff --git a/package.json b/package.json index d1096c3..d9647da 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gallery-badmanners-xyz", "type": "module", - "version": "1.5.0", + "version": "1.5.1", "scripts": { "dev": "astro dev", "start": "astro dev", @@ -20,7 +20,7 @@ "@tailwindcss/typography": "^0.5.12", "@types/sanitize-html": "^2.11.0", "astro": "^4.5.16", - "astro-pagefind": "^1.5.0", + "astro-pagefind": "^1.6.0", "github-slugger": "^2.0.0", "marked": "^12.0.1", "pagefind": "^1.1.0", diff --git a/public/.htaccess b/public/.htaccess index de377bf..41b8d2d 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -1 +1,2 @@ ErrorDocument 404 /404.html +RedirectMatch 301 ^/stories(\/(index.html)?)?$ /stories/1/ diff --git a/src/components/MastodonComments.astro b/src/components/MastodonComments.astro index 2744def..fd8fbbc 100644 --- a/src/components/MastodonComments.astro +++ b/src/components/MastodonComments.astro @@ -53,7 +53,7 @@ const { instance, user, postId } = Astro.props; <span class="mr-1" data-publish-date></span> </a> </div> - <div data-content class="prose-a:text-link prose prose-story my-1 dark:prose-invert prose-img:my-0"></div> + <div data-content class="prose-a:text-link prose-story prose my-1 dark:prose-invert prose-img:my-0"></div> <div class="ml-1 flex flex-row pb-2 pt-1"> <div class="flex" aria-label="Favorites"> <span data-favorites></span> diff --git a/src/components/Prose.astro b/src/components/Prose.astro index 91a1cb8..cf16681 100644 --- a/src/components/Prose.astro +++ b/src/components/Prose.astro @@ -2,6 +2,6 @@ --- -<div class="prose-a:text-link prose prose-story max-w-none dark:prose-invert"> +<div class="prose-a:text-link prose prose-bm max-w-none dark:prose-invert"> <slot /> </div> diff --git a/src/content/config.ts b/src/content/config.ts index 3654509..3ea19e6 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -185,10 +185,14 @@ const tagCategoriesCollection = defineCollection({ name: z.string(), index: z.number().int(), tags: z.array( - z.union([ - z.string(), - z.record(lang, z.string()).refine((tag) => "eng" in tag, `object-formatted tag must have an "eng" key`), - ]), + z.object({ + name: z.union([ + z.string(), + z.record(lang, z.string()).refine((tag) => "eng" in tag, `object-formatted tag must have an "eng" key`), + ]), + description: z.string().optional(), + related: z.array(z.string()).optional(), + }), ), }), }); diff --git a/src/content/games/crossing-over.md b/src/content/games/crossing-over.md index 12a55a4..b74eb41 100644 --- a/src/content/games/crossing-over.md +++ b/src/content/games/crossing-over.md @@ -35,9 +35,9 @@ platforms: posts: eka: https://aryion.com/g4/view/986297 furaffinity: https://www.furaffinity.net/view/55712675/ - weasyl: https://www.weasyl.com/~badmanners/submissions/2356092/crossing-over-vore-game inkbunny: https://inkbunny.net/s/3262911 sofurry: https://www.sofurry.com/view/2109688 + weasyl: https://www.weasyl.com/~badmanners/submissions/2356092/crossing-over-vore-game mastodon: https://meow.social/@BadManners/112009918919441027 tags: - oral vore diff --git a/src/content/stories/rose-s-binge.md b/src/content/stories/rose-s-binge.md index f2559c3..689649d 100644 --- a/src/content/stories/rose-s-binge.md +++ b/src/content/stories/rose-s-binge.md @@ -10,6 +10,13 @@ description: | A seemingly insatiable plushie makes short work of an entire town, and the last few people left will try their best to escape from their binge. This is a commission for Dee! As a disclaimer, all characters, implied or otherwise, are adults. +posts: + eka: https://aryion.com/g4/view/1030875 + furaffinity: https://www.furaffinity.net/view/57459926/ + inkbunny: https://inkbunny.net/s/3377727 + sofurry: https://www.sofurry.com/view/2156241 + weasyl: https://www.weasyl.com/~badmanners/submissions/2396279/c-rose-s-binge + mastodon: https://meow.social/@BadManners/112827108738415986 tags: - oral vore - cock vore diff --git a/src/content/stories/tiny-accident.md b/src/content/stories/tiny-accident.md index c84d887..3c390ac 100644 --- a/src/content/stories/tiny-accident.md +++ b/src/content/stories/tiny-accident.md @@ -17,10 +17,10 @@ descriptionPlaintext: > posts: eka: https://aryion.com/g4/view/994229 furaffinity: https://www.furaffinity.net/view/56026627/ - weasyl: https://www.weasyl.com/~badmanners/submissions/2363560/tiny-accident inkbunny: https://inkbunny.net/s/3283508 - mastodon: https://meow.social/@BadManners/112157812554023271 sofurry: https://www.sofurry.com/view/2118138 + weasyl: https://www.weasyl.com/~badmanners/submissions/2363560/tiny-accident + mastodon: https://meow.social/@BadManners/112157812554023271 tags: - anthro predator - anthro prey diff --git a/src/content/stories/tomo-moku.md b/src/content/stories/tomo-moku.md index 8d4c073..a95fc3d 100644 --- a/src/content/stories/tomo-moku.md +++ b/src/content/stories/tomo-moku.md @@ -25,6 +25,7 @@ tags: - willing predator - unwilling prey - flash fiction + - toki pona summary: | Lijan, a red nondescript mammal, makes his way through town to a new 'tomo moku' ("eating place"), expecting it to be a restaurant. They can't find the building among the similar looking ones, and spot a colorful and spooked bird covered in some liquid, who they ask for directions. diff --git a/src/content/stories/woofer-exploration.md b/src/content/stories/woofer-exploration.md index 26a6fbd..7ae6766 100644 --- a/src/content/stories/woofer-exploration.md +++ b/src/content/stories/woofer-exploration.md @@ -14,6 +14,13 @@ descriptionPlaintext: > The Director wakes up in the middle of the night to a little intruder, and decides to have some fun with him. This was a gift for my friend Hans! This story is sort of a non-canon sequel to his game "Director Explorer" on Itch.io, which you should definitely try out if you haven't already! +posts: + eka: https://aryion.com/g4/view/1030872 + furaffinity: https://www.furaffinity.net/view/57459921/ + inkbunny: https://inkbunny.net/s/3377722 + sofurry: https://www.sofurry.com/view/2156237 + weasyl: https://www.weasyl.com/~badmanners/submissions/2396278/woofer-exploration + mastodon: https://meow.social/@BadManners/112827104119035982 tags: - Sam Brendan - oral vore diff --git a/src/content/tag-categories/body-types.yaml b/src/content/tag-categories/body-types.yaml index 8437184..8c19cce 100644 --- a/src/content/tag-categories/body-types.yaml +++ b/src/content/tag-categories/body-types.yaml @@ -1,13 +1,19 @@ name: Body types index: 1 tags: - - anthro predator - - feral predator - - taur predator - - eng: ambiguous predator - tok: sijelo pi jan pi wawa mute li ale - - human prey - - anthro prey - - feral prey - - eng: ambiguous prey - tok: sijelo pi jan pi wawa lili li ale + - name: anthro predator + description: Scenarios where at least one of the predators is an anthropomorphic animal, i.e. generally regarded as a "furry". + - name: feral predator + description: Scenarios where at least one of the predators is an animal based on a real or mythological creature. + - name: taur predator + description: Scenarios where at least one of the predators is a multi-legged centaur-like creature, with an animal lower body and anthropomorphic upper body. + - name: { eng: ambiguous predator, tok: sijelo pi jan pi wawa mute li ale } + description: Scenarios where the body type of at least one of the predators is left ambiguous. + - name: human prey + description: Scenarios where at least one of the prey is a human person. + - name: anthro prey + description: Scenarios where at least one of the prey is an anthropomorphic animal, i.e. generally regarded as a "furry". + - name: feral prey + description: Scenarios where at least one of the predators is an animal based on a real or mythological creature. + - name: { eng: ambiguous prey, tok: sijelo pi jan pi wawa lili li ale } + description: Scenarios where the body type of at least one of the predators is left ambiguous. diff --git a/src/content/tag-categories/genders.yaml b/src/content/tag-categories/genders.yaml index cbce725..fd14dab 100644 --- a/src/content/tag-categories/genders.yaml +++ b/src/content/tag-categories/genders.yaml @@ -1,15 +1,31 @@ name: Genders index: 2 tags: - - male predator - - trans male predator - - female predator - - non-binary predator - - eng: ambiguous gender predator - tok: jan pi wawa mute li meli anu mije - - male prey - - female prey - - trans female prey - - non-binary prey - - eng: ambiguous gender prey - tok: jan pi wawa lili li meli anu mije + - name: male predator + description: Scenarios where at least one of the predators is a man and/or male-presenting. + related: + - trans male predator + - name: trans male predator + description: Scenarios where at least one of the predators is a man and/or male-presenting; more specifically, undergoing or or having undergone gender transition. + related: + - male predator + - name: female predator + description: Scenarios where at least one of the predators is a woman and/or female-presenting. + - name: non-binary predator + description: Scenarios where at least one of the predators has a non-binary gender expression, be they genderless/agender, intersex, androgynous, gender-fluid, non-binary and/or non-binary-presenting, et cetera, regardless of undergoing or having undergone gender transition or not. + - name: { eng: ambiguous gender predator, tok: jan pi wawa mute li meli anu mije } + description: Scenarios where the gender at least one of the predators is left ambiguous. + - name: male prey + description: Scenarios where at least one of the prey is a man and/or male-presenting. + - name: female prey + description: Scenarios where at least one of the prey is a woman and/or female-presenting. + related: + - trans female prey + - name: trans female prey + description: Scenarios where at least one of the prey is a woman and/or female-presenting; more specifically, undergoing or having undergone gender transition. + related: + - female prey + - name: non-binary prey + description: Scenarios where at least one of the predators has a non-binary gender expression, be they genderless/agender, intersex, androgynous, gender-fluid, non-binary and/or non-binary-presenting, et cetera, regardless of undergoing or having undergone gender transition or not. + - name: { eng: ambiguous gender prey, tok: jan pi wawa lili li meli anu mije } + description: Scenarios where the gender at least one of the predators is left ambiguous. diff --git a/src/content/tag-categories/other-kinks.yaml b/src/content/tag-categories/other-kinks.yaml index b45523b..10cf7ef 100644 --- a/src/content/tag-categories/other-kinks.yaml +++ b/src/content/tag-categories/other-kinks.yaml @@ -1,13 +1,25 @@ name: Other kinks index: 7 tags: - - hyper - - egg play - - transformation - - netorare - - sizeplay - - inflation - - daddy play - - BDSM - - dubcon - - plushie + - name: hyper + description: Scenarios where a character has abnormally large features compared to their body, usually genitalia. + - name: egg play + description: Scenarios with sexual action involving eggs. + related: + - object vore + - name: transformation + description: Scenarios where a character changes body and/or species. + - name: netorare + description: Scenarios involving a character cheating on their partner with or without consent, and/or cuckoldry. + - name: sizeplay + description: Scenarios where a character changes size. + - name: inflation + description: Scenarios where a character expands out with the excessive intake of fluids, including semen. + - name: daddy play + description: Scenarios where a character refers to another as their "daddy" in a kinky manner as part of a sexual situation. + - name: BDSM + description: Scenarios related to bondage, sadism, masochism, dominance, and/or submission in a kinky manner. + - name: dubcon + description: Scenarios where sexual consent is dubious. + - name: plushie + description: Scenarios with sexual action involving stuffed toys or plushie-like characters. diff --git a/src/content/tag-categories/recurring-characters.yaml b/src/content/tag-categories/recurring-characters.yaml index b52fef2..04771b0 100644 --- a/src/content/tag-categories/recurring-characters.yaml +++ b/src/content/tag-categories/recurring-characters.yaml @@ -1,6 +1,9 @@ name: Recurring characters index: 9 tags: - - Sam Brendan - - Beetle - - Muno + - name: Sam Brendan + description: Content that includes my character and fursona [Sam, the mimic x maned wolf hybrid](https://badmanners.xyz/sam_brendan). + - name: Beetle + description: Content that includes my character [Beetle, the gryphon](https://booru.badmanners.xyz/index.php?q=post/view/3). + - name: Muno + description: Content that includes my character Muno, the snake. diff --git a/src/content/tag-categories/relative-size.yaml b/src/content/tag-categories/relative-size.yaml index 0a5a417..5ddbcb3 100644 --- a/src/content/tag-categories/relative-size.yaml +++ b/src/content/tag-categories/relative-size.yaml @@ -1,9 +1,17 @@ name: Relative size index: 3 tags: - - macro predator - - micro prey - - size difference - - similar size - - same size - - smaller predator + - name: macro predator + description: Scenarios where at least one of the predators has a size/height one or more orders of magnitude larger than average. + - name: micro prey + description: Scenarios where at least one of the prey has a size/height one or more orders of magnitude smaller than average. + - name: size difference + description: Scenarios where at least one of the predators has a notably larger size/height than one of their prey. + - name: similar size + description: Scenarios where at least one of the predators has a slightly larger size/height than one of their prey. + - name: same size + description: Scenarios where at least one of the predators has the same size/height as one of their prey, or their sizes are comparable. + - name: smaller predator + description: Scenarios where at least one of the predators has a notably smaller size/height than one of their prey. + related: + - role reversal diff --git a/src/content/tag-categories/sexual-content.yaml b/src/content/tag-categories/sexual-content.yaml index 7fb9f88..219cd3f 100644 --- a/src/content/tag-categories/sexual-content.yaml +++ b/src/content/tag-categories/sexual-content.yaml @@ -1,9 +1,25 @@ name: Sexual content index: 6 tags: - - nudity - - masturbation - - straight sex - - gay sex - - lesbian sex - - orgy + - name: nudity + description: Scenarios where a character is nude and displaying their sexual features, without necessarily participating in a sexual situation. + - name: masturbation + description: Scenarios where a character sexually stimulates themselves. + - name: straight sex + description: Scenarios where two or more characters of different genders participate in sexual activity. + related: + - orgy + - name: gay sex + description: Scenarios where two or more characters of the same gender, both men and/or male-presenting, participate in sexual activity. + related: + - orgy + - name: lesbian sex + description: Scenarios where two or more characters of the same gender, both women and/or female-presenting, participate in sexual activity. + related: + - orgy + - name: orgy + description: Scenarios where more than two characters participate in sexual activity freely. + related: + - straight sex + - gay sex + - lesbian sex diff --git a/src/content/tag-categories/type-of-content.yaml b/src/content/tag-categories/type-of-content.yaml index 4f1714f..849bc9c 100644 --- a/src/content/tag-categories/type-of-content.yaml +++ b/src/content/tag-categories/type-of-content.yaml @@ -1,7 +1,11 @@ name: Type of content index: 8 tags: - - request - - commission - - eng: flash fiction - tok: lipu lili + - name: request + description: Stories made as part of someone else's request as a gift. + - name: commission + description: Stories made as part of a commission to someone else. + - name: { eng: flash fiction, tok: lipu lili } + description: Short-format stories of no more than 2,500 words. + - name: toki pona + description: Stories written in toki pona, the language of good. diff --git a/src/content/tag-categories/types-of-vore.yaml b/src/content/tag-categories/types-of-vore.yaml index 7c16509..1c9df3e 100644 --- a/src/content/tag-categories/types-of-vore.yaml +++ b/src/content/tag-categories/types-of-vore.yaml @@ -1,13 +1,21 @@ name: Types of vore index: 0 tags: - - eng: oral vore - tok: moku musi kepeken uta - - anal vore - - cock vore - - unbirth - - tail vore - - slit vore - - sheath vore - - nipple vore - - chest maw vore + - name: { eng: oral vore, tok: moku musi kepeken uta } + description: Scenarios where prey are consumed by the predator through their mouth. + - name: anal vore + description: Scenarios where prey are consumed by the predator through their butt/anus. + - name: cock vore + description: Scenarios where prey are consumed by the predator through their penis. + - name: unbirth + description: Scenarios where prey are consumed by the predator through their vagina/vulva. + - name: tail vore + description: Scenarios where prey are consumed by the predator through an opening or mouth in their tail. + - name: slit vore + description: Scenarios where prey are consumed by the predator through a genital slit. + - name: sheath vore + description: Scenarios where prey are consumed by the predator through a genital sheath, generally around their penis. + - name: nipple vore + description: Scenarios where prey are consumed by the predator through their nipple/breast. + - name: chest maw vore + description: Scenarios where prey are consumed by the predator through a mouth/maw on their chest cavity. diff --git a/src/content/tag-categories/vore-related-scenarios.yaml b/src/content/tag-categories/vore-related-scenarios.yaml index a9147b2..f6f6d61 100644 --- a/src/content/tag-categories/vore-related-scenarios.yaml +++ b/src/content/tag-categories/vore-related-scenarios.yaml @@ -1,20 +1,67 @@ name: Vore-related scenarios index: 5 tags: - - point of view - - long-term endo - - perma endo - - implied perma endo - - full tour - - implied full tour - - regurgitation - - implied regurgitation - - prey transfer - - object vore - - role reversal - - nested vore - - multiple prey - - messy stomach - - hammerspace vore - - bladder vore - - soul vore + - name: point of view + description: Scenarios where the narration takes the perspective of one of the characters, generally in first person. + - name: long-term endo + description: Scenarios where prey remains safely inside of their predator for an extended period of time. + related: + - perma endo + - implied perma endo + - name: perma endo + description: Scenarios where prey remains safely inside of their predator for an indefinite period of time. + related: + - long-term endo + - implied perma endo + - name: implied perma endo + description: Scenarios where prey is implied to remain safely inside of their predator for an indefinite period of time. + related: + - long-term endo + - perma endo + - name: full tour + description: Scenarios where prey passes all the way through their predator's gastrointestinal system, entering through one end and exiting through the opposite one. + related: + - implied full tour + - name: implied full tour + description: Scenarios where prey is implied to pass all the way through their predator's gastrointestinal system, entering through one end and supposedly exiting through the opposite one. + related: + - full tour + - name: regurgitation + description: Scenarios where prey are explicitly let out through the same spot they entered their predator, specifically their mouth. + related: + - implied regurgitation + - oral vore + - name: implied regurgitation + description: Scenarios where prey are implied to have been let out through the same spot they entered their predator, specifically their mouth. + related: + - regurgitation + - oral vore + - name: prey transfer + description: Scenarios where prey are transferred from one predator into another, or transferred from one part of their predator to another. + - name: object vore + description: Scenarios where predators consume objects, alongside their prey or in place of them. + - name: role reversal + description: Scenarios where predators become prey and vice-versa, where dominant characters become prey, or where submissive characters become predators. + related: + - smaller predator + - name: nested vore + description: Scenarios where a predator becomes prey to another predator, or a prey consumes another prey. + related: + - multiple prey + - role reversal + - name: multiple prey + description: Scenarios where one predator consumes multiple prey at once. + - name: messy stomach + description: Scenarios where a prey visits a messy stomach, usually with semi-digested food inside. + related: + - oral vore + - name: hammerspace vore + description: Scenarios where a predator's body doesn't grow proportionally around the prey they consume, either at a substantially lower rate or not at all. + related: + - multiple prey + - name: bladder vore + description: Scenarios where prey consumed through the predator's urethra end up in their bladder. + related: + - cock vore + - name: soul vore + description: Scenarios where predators consume a soul instead of their prey's body. diff --git a/src/content/tag-categories/willingness.yaml b/src/content/tag-categories/willingness.yaml index 68c43d3..1b1edd6 100644 --- a/src/content/tag-categories/willingness.yaml +++ b/src/content/tag-categories/willingness.yaml @@ -1,13 +1,19 @@ name: Willingness index: 4 tags: - - eng: willing predator - tok: jan pi wawa mute li wile e moku musi - - semi-willing predator - - unwilling predator - - asleep predator - - willing prey - - semi-willing prey - - eng: unwilling prey - tok: jan pi wawa lili li wile ala e moku musi - - asleep prey + - name: { eng: willing predator, tok: jan pi wawa mute li wile e moku musi } + description: Scenarios where at least one of the predators participates in vore willingly. + - name: semi-willing predator + description: Scenarios where the willingness of at least one of the predators might be partial, oscillating between willing and unwilling, somewhere in-between, or ambiguous. + - name: unwilling predator + description: Scenarios where at least one of the predators participates in vore unwillingly. + - name: asleep predator + description: Scenarios where at least one of the predators participates in vore while asleep. + - name: willing prey + description: Scenarios where at least one of the prey participates in vore willingly. + - name: semi-willing prey + description: Scenarios where the willingness of at least one of the prey might be partial, oscillating between willing and unwilling, somewhere in-between, or ambiguous. + - name: { eng: unwilling prey, tok: jan pi wawa lili li wile ala e moku musi } + description: Scenarios where at least one of the prey participates in vore unwillingly. + - name: asleep prey + description: Scenarios where at least one of the predators participates in vore while asleep. diff --git a/src/i18n/index.ts b/src/i18n/index.ts index b8e1532..143c6f0 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -151,6 +151,10 @@ export const UI_STRINGS: Record<string, TranslationRecord> = { "game/platform_ios": { eng: "iOS", }, + "game/warnings": { + eng: (platforms: string[], contentWarning: string) => + `${(UI_STRINGS["game/platforms"]!.eng as (arg: string[]) => string)(platforms)}. ${contentWarning}`, + }, }; export function t(lang: Lang, stringOrSource: string | TranslationRecord, ...args: any[]): string { diff --git a/src/layouts/GameLayout.astro b/src/layouts/GameLayout.astro index 345e2fd..bc1a1a5 100644 --- a/src/layouts/GameLayout.astro +++ b/src/layouts/GameLayout.astro @@ -22,12 +22,9 @@ const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrighte // const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft); const categorizedTags = Object.fromEntries( (await getCollection("tag-categories")).flatMap((category) => - category.data.tags.map((tag) => { - if (typeof tag === "string") { - return [tag, tag]; - } - return [t(DEFAULT_LANG, tag as any), t(props.lang, tag as any)]; - }), + category.data.tags.map<[string, string]>(({ name }) => + typeof name === "string" ? [name, name] : [t(DEFAULT_LANG, name as any), t(props.lang, name as any)], + ), ), ); const tags = props.tags.map<[string, string]>((tag) => { diff --git a/src/layouts/StoryLayout.astro b/src/layouts/StoryLayout.astro index afd9f90..ef39974 100644 --- a/src/layouts/StoryLayout.astro +++ b/src/layouts/StoryLayout.astro @@ -34,12 +34,9 @@ const relatedStories = (await getEntries(props.relatedStories)).filter((story) = // const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft); const categorizedTags = Object.fromEntries( (await getCollection("tag-categories")).flatMap((category) => - category.data.tags.map((tag) => { - if (typeof tag === "string") { - return [tag, tag]; - } - return [t(DEFAULT_LANG, tag as any), t(props.lang, tag as any)]; - }), + category.data.tags.map<[string, string]>(({ name }) => + typeof name === "string" ? [name, name] : [t(DEFAULT_LANG, name as any), t(props.lang, name as any)], + ), ), ); const tags = props.tags.map<[string, string]>((tag) => { diff --git a/src/pages/api/export-story/[...slug].ts b/src/pages/api/export-story/[...slug].ts index f9b5903..afc20d3 100644 --- a/src/pages/api/export-story/[...slug].ts +++ b/src/pages/api/export-story/[...slug].ts @@ -1,13 +1,12 @@ import type { APIRoute, GetStaticPaths } from "astro"; import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro:content"; -import { marked, type RendererApi } from "marked"; -import { decode as tinyDecode } from "tiny-decode"; import type { Lang, Website } from "../../../content/config"; import { t } from "../../../i18n"; import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters"; +import { markdownToBbcode } from "../../../utils/markdown_to_bbcode"; interface ExportWebsiteInfo { - website: string; + website: Website; exportFormat: "bbcode" | "markdown"; } @@ -21,37 +20,6 @@ const WEBSITE_LIST = [ type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: infer K }> ? K : never; -//type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<[infer K, DescriptionExportFormat]> ? K : never; - -const bbcodeRenderer: RendererApi = { - strong: (text) => `[b]${text}[/b]`, - em: (text) => `[i]${text}[/i]`, - codespan: (code) => code, - br: () => `\n\n`, - link: (href, _, text) => `[url=${href}]${text}[/url]`, - image: (href) => `[img]${href}[/img]`, - text: (text) => text, - paragraph: (text) => `\n${text}\n`, - list: (body, ordered) => (ordered ? `\n[ol]\n${body}[/ol]\n` : `\n[ul]\n${body}[/ul]\n`), - listitem: (text) => `[li]${text}[/li]\n`, - blockquote: (quote) => `\n[quote]${quote}[/quote]\n`, - code: (code) => `\n[code]${code}[/code]\n`, - heading: (heading) => `\n${heading}\n`, - table: (header, body) => `\n[table]\n${header}${body}[/table]\n`, - tablerow: (content) => `[tr]\n${content}[/tr]\n`, - tablecell: (content, { header }) => (header ? `[th]${content}[/th]\n` : `[td]${content}[/td]\n`), - hr: () => `\n===\n`, - del: () => { - throw new Error("Not supported by bbcodeRenderer: del"); - }, - html: () => { - throw new Error("Not supported by bbcodeRenderer: html"); - }, - checkbox: () => { - throw new Error("Not supported by bbcodeRenderer: checkbox"); - }, -}; - function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website): string { const link = user.data.links[website]; if (link) { @@ -147,7 +115,7 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa if ("weasyl" in user.data.links) { return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`; } else if (isPreferredWebsite(user, "furaffinity")) { - return `<fa:${getUsernameForWebsite(user, "furaffinity")}>`; + return `<fa:${getUsernameForWebsite(user, "furaffinity").replaceAll("_", "")}>`; } else if (isPreferredWebsite(user, "inkbunny")) { return `<ib:${getUsernameForWebsite(user, "inkbunny")}>`; } else if (isPreferredWebsite(user, "sofurry")) { @@ -220,47 +188,40 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) = const anonymousUser = await getEntry("users", "anonymous"); const anonymousFallback = getNameForUser(anonymousUser, anonymousUser, lang); - const description: Record<ExportWebsiteName, string> = Object.fromEntries( - await Promise.all( - WEBSITE_LIST.map(async ({ website, exportFormat }) => { - const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website, anonymousFallback); - const storyDescription = ( - [ - story.data.description, - `*${t(lang, "story/warnings", story.data.wordCount || "???", story.data.contentWarning.trim())}*`, - t( - lang, - "export_story/writing", - authorsList.map((author) => u(author)), - ), - requester && t(lang, "export_story/request_for", u(requester)), - commissioner && t(lang, "export_story/commissioned_by", u(commissioner)), - ...copyrightedCharacters.map(([user, characterList]) => - characterList[0] == "" - ? t(lang, "characters/all_characters_are_copyrighted_by", u(user)) - : t(lang, "characters/characters_are_copyrighted_by", u(user), characterList), - ), - ].filter((data) => data) as string[] - ) - .join("\n\n") - .replaceAll( - /\[([^\]]+)\]\((\/[^\)]+)\)/g, - (_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`, - ); - if (exportFormat === "bbcode") { - return [ - website, - tinyDecode(await marked.use({ renderer: bbcodeRenderer }).parse(storyDescription)) - .replaceAll(/\n\n\n+/g, "\n\n") - .trim(), - ]; - } - if (exportFormat === "markdown") { - return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()]; - } - throw new Error(`Unhandled ExportFormat "${exportFormat}"`); - }), - ), + const description = Object.fromEntries( + WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => { + const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website, anonymousFallback); + const storyDescription = ( + [ + story.data.description, + `*${t(lang, "story/warnings", story.data.wordCount || "???", story.data.contentWarning.trim())}*`, + t( + lang, + "export_story/writing", + authorsList.map((author) => u(author)), + ), + requester && t(lang, "export_story/request_for", u(requester)), + commissioner && t(lang, "export_story/commissioned_by", u(commissioner)), + ...copyrightedCharacters.map(([user, characterList]) => + characterList[0] == "" + ? t(lang, "characters/all_characters_are_copyrighted_by", u(user)) + : t(lang, "characters/characters_are_copyrighted_by", u(user), characterList), + ), + ].filter((data) => data) as string[] + ) + .join("\n\n") + .replaceAll( + /\[([^\]]+)\]\((\/[^\)]+)\)/g, + (_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`, + ); + if (exportFormat === "bbcode") { + return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n").trim()]; + } else if (exportFormat === "markdown") { + return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()]; + } else { + throw new Error(`Unhandled export format "${exportFormat}"`); + } + }) ); const storyHeader = diff --git a/src/pages/feed.xml.ts b/src/pages/feed.xml.ts index 533413e..e5ea5b4 100644 --- a/src/pages/feed.xml.ts +++ b/src/pages/feed.xml.ts @@ -84,7 +84,7 @@ export const GET: APIRoute = async ({ site }) => { pubDate: toNoonUTCDate(data.pubDate!), link: `/games/${slug}`, description: - `${t(data.lang, "game/platforms", data.platforms)}. ${data.contentWarning} ${data.descriptionPlaintext || data.description}` + `${t(data.lang, "game/warnings", data.platforms, data.contentWarning)} ${data.descriptionPlaintext || data.description}` .replaceAll(/[\n ]+/g, " ") .trim(), categories: ["game"], @@ -104,8 +104,8 @@ export const GET: APIRoute = async ({ site }) => { )}</p>` + `<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` + `<hr><p><em>${data.contentWarning.trim()}</em></p>` + - `<hr>${tinyDecode(await marked(body))}` + - `<hr>${tinyDecode(await marked(data.description))}`, + `<hr>${tinyDecode(marked.parse(body) as string)}` + + `<hr>${tinyDecode(marked.parse(data.description) as string)}`, ), })), ), diff --git a/src/pages/games.astro b/src/pages/games.astro index a157ceb..9304079 100644 --- a/src/pages/games.astro +++ b/src/pages/games.astro @@ -2,6 +2,7 @@ import { Image } from "astro:assets"; import { getCollection } from "astro:content"; import GalleryLayout from "../layouts/GalleryLayout.astro"; +import { t } from "../i18n"; const games = (await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)).sort( (a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime(), @@ -16,7 +17,11 @@ const games = (await getCollection("games", (game) => !game.data.isDraft && game { games.map((game) => ( <li> - <a class="text-link hover:underline focus:underline" href={`/games/${game.slug}`}> + <a + class="text-link hover:underline focus:underline" + href={`/games/${game.slug}`} + title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim())} + > {game.data.thumbnail ? ( <div class="flex aspect-[630/500] max-w-[288px] justify-center"> <Image class="m-auto" src={game.data.thumbnail} alt={`Thumbnail for ${game.data.title}`} width={288} /> diff --git a/src/pages/index.astro b/src/pages/index.astro index e8589ee..2ac7fca 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -2,6 +2,7 @@ import { type CollectionEntry, getCollection } from "astro:content"; import { Image } from "astro:assets"; import GalleryLayout from "../layouts/GalleryLayout.astro"; +import { t } from "../i18n"; const MAX_ITEMS = 8; @@ -10,6 +11,7 @@ interface LatestItemsEntry { thumbnail: CollectionEntry<"stories">["data"]["thumbnail"]; href: string; title: string; + altText: string; pubDate: Date; } @@ -26,6 +28,7 @@ const latestItems: LatestItemsEntry[] = [ thumbnail: story.data.thumbnail, href: `/stories/${story.slug}`, title: story.data.title, + altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim()), pubDate: story.data.pubDate!, })), games.map<LatestItemsEntry>((game) => ({ @@ -33,6 +36,7 @@ const latestItems: LatestItemsEntry[] = [ thumbnail: game.data.thumbnail, href: `/games/${game.slug}`, title: game.data.title, + altText: t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim()), pubDate: game.data.pubDate!, })), ] @@ -63,7 +67,7 @@ const latestItems: LatestItemsEntry[] = [ { latestItems.map((entry) => ( <li class="break-inside-avoid"> - <a class="text-link hover:underline focus:underline" href={entry.href}> + <a class="text-link hover:underline focus:underline" href={entry.href} title={entry.altText}> {entry.thumbnail ? ( <div class="flex aspect-square max-w-[192px] justify-center"> <Image class="m-auto" src={entry.thumbnail} alt={`Thumbnail for ${entry.title}`} width={192} /> diff --git a/src/pages/stories/[page].astro b/src/pages/stories/[page].astro index b581b2c..4ab3aa7 100644 --- a/src/pages/stories/[page].astro +++ b/src/pages/stories/[page].astro @@ -4,6 +4,7 @@ import { Image } from "astro:assets"; import { getCollection } from "astro:content"; import GalleryLayout from "../../layouts/GalleryLayout.astro"; import type { CollectionEntry } from "astro:content"; +import { t } from "../../i18n"; type Props = { page: Page<CollectionEntry<"stories">>; @@ -67,7 +68,11 @@ const totalPages = Math.ceil(page.total / page.size); { page.data.map((story) => ( <li class="break-inside-avoid"> - <a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}> + <a + class="text-link hover:underline focus:underline" + href={`/stories/${story.slug}`} + title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())} + > {story.data.thumbnail ? ( <div class="flex aspect-square max-w-[192px] justify-center"> <Image diff --git a/src/pages/tags.astro b/src/pages/tags.astro index 3b6aa86..e86937f 100644 --- a/src/pages/tags.astro +++ b/src/pages/tags.astro @@ -2,6 +2,7 @@ import { getCollection } from "astro:content"; import { slug } from "github-slugger"; import GalleryLayout from "../layouts/GalleryLayout.astro"; +import { markdownToPlaintext } from "../utils/markdown_to_plaintext"; const [stories, games, tagCategories] = await Promise.all([ getCollection("stories"), @@ -30,35 +31,43 @@ const seriesCollection = await getCollection("series"); }); const uncategorizedTagsSet = new Set(tagsSet); -const categorizedTags: Array<[string, string, string[]]> = tagCategories - .sort((a, b) => a.data.index - b.data.index) - .map((category) => { - const tagList = category.data.tags.map((tag) => (typeof tag === "string" ? tag : tag["eng"]!)); - tagList.forEach((tag, index) => { - if (index !== tagList.findLastIndex((val) => tag == val)) { - throw new Error(`Duplicated tag "${tag}" found in multiple tag-categories`); - } - }); - return [ - category.data.name, - category.id, - tagList.filter((tag) => { - if (draftOnlyTagsSet.has(tag)) { - console.log(`Omitting draft-only tag "${tag}"`); - return false; +const categorizedTags: Array<[string, string, [string, string | undefined][]]> = await Promise.all( + tagCategories + .sort((a, b) => a.data.index - b.data.index) + .map(async (category) => { + const tagList = category.data.tags.map(({ name, description }) => { + description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ").trim(); + return (typeof name === "string" ? [name, description] : [name["eng"]!, description]) as [ + string, + string | undefined, + ]; + }); + tagList.forEach(([tag, _], index) => { + if (index !== tagList.findLastIndex(([otherTag, _]) => tag == otherTag)) { + throw new Error(`Duplicated tag "${tag}" found in multiple tag-categories lists`); } - if (tagsSet.has(tag)) { - uncategorizedTagsSet.delete(tag); - } - return true; - }), - ]; - }); + }); + return [ + category.data.name, + category.id, + tagList.filter(([tag, _]) => { + if (draftOnlyTagsSet.has(tag)) { + console.log(`Omitting draft-only tag "${tag}"`); + return false; + } + if (tagsSet.has(tag)) { + uncategorizedTagsSet.delete(tag); + } + return true; + }), + ]; + }), +); if (uncategorizedTagsSet.size > 0) { const tagList = [...uncategorizedTagsSet]; - console.log("The following tags have no category:", tagList); - categorizedTags.push(["Uncategorized tags", "others", tagList]); + console.warn("The following tags have no category:", tagList); + // categorizedTags.push(["Uncategorized tags", "others", tagList]); } --- @@ -92,9 +101,9 @@ if (uncategorizedTagsSet.size > 0) { {category} </h2> <ul class="flex max-w-3xl flex-wrap gap-x-2 gap-y-2 px-2"> - {tagList.map((tag) => ( + {tagList.map(([tag, description]) => ( <li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white"> - <a class="hover:underline focus:underline" href={`/tags/${slug(tag)}`}> + <a class="hover:underline focus:underline" href={`/tags/${slug(tag)}`} title={description}> {tag} </a> </li> diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro index f9fac85..817adc4 100644 --- a/src/pages/tags/[slug].astro +++ b/src/pages/tags/[slug].astro @@ -2,11 +2,16 @@ import type { GetStaticPaths } from "astro"; import { Image } from "astro:assets"; import { type CollectionEntry, getCollection } from "astro:content"; +import { Markdown } from "@astropub/md"; import { slug } from "github-slugger"; import GalleryLayout from "../../layouts/GalleryLayout.astro"; +import Prose from "../../components/Prose.astro"; +import { t } from "../../i18n"; type Props = { tag: string; + description?: string; + related?: string[]; stories: CollectionEntry<"stories">[]; games: CollectionEntry<"games">[]; }; @@ -16,10 +21,11 @@ type Params = { }; export const getStaticPaths: GetStaticPaths = async () => { - const [stories, games, series] = await Promise.all([ + const [stories, games, series, tagCategories] = await Promise.all([ getCollection("stories"), getCollection("games"), getCollection("series"), + getCollection("tag-categories"), ]); const seriesTags = new Set(series.map((s) => s.data.name)); const tags = new Set<string>(); @@ -33,12 +39,40 @@ export const getStaticPaths: GetStaticPaths = async () => { tags.add(tag); }); }); + const tagDescriptions = Object.fromEntries( + tagCategories.flatMap((category) => + category.data.tags.reduce( + (acc, { name, description, related }) => { + if (related) { + related = related.filter((relatedTag) => { + if (relatedTag == name) { + console.warn(`Tag "${name}" should not have itself as a related tag; removing`); + return false; + } + if (!tags.has(relatedTag)) { + console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`); + return false; + } + return true; + }); + } + acc.push( + typeof name === "string" ? [name, { description, related }] : [name["eng"]!, { description, related }], + ); + return acc; + }, + [] as [string, { description: string | undefined; related: string[] | undefined }][], + ), + ), + ); return [...tags] .filter((tag) => !seriesTags.has(tag)) .map((tag) => ({ params: { slug: slug(tag) } satisfies Params, props: { tag, + description: tagDescriptions[tag].description, + related: tagDescriptions[tag].related, stories: stories .filter((story) => !story.data.isDraft && story.data.pubDate && story.data.tags.includes(tag)) .sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime()), @@ -49,28 +83,43 @@ export const getStaticPaths: GetStaticPaths = async () => { })); }; -const { tag, stories, games } = Astro.props; +const { tag, description, stories, games, related } = Astro.props; +if (!description) { + console.warn(`Tag "${tag}" has no description!`); +} const count = stories.length + games.length; -let tagDescription: string = ""; +let totalWorksWithTag: string = ""; if (count == 1) { if (stories.length == 1) { - tagDescription = `One story tagged with "${tag}".`; + totalWorksWithTag = `One story tagged with "${tag}".`; } else if (games.length == 1) { - tagDescription = `One game tagged with "${tag}".`; + totalWorksWithTag = `One game tagged with "${tag}".`; } } else if (stories.length == 0) { - tagDescription = `${games.length} games tagged with "${tag}".`; + totalWorksWithTag = `${games.length} games tagged with "${tag}".`; } else if (games.length == 0) { - tagDescription = `${stories.length} stories tagged with "${tag}".`; + totalWorksWithTag = `${stories.length} stories tagged with "${tag}".`; } else { - tagDescription = `${stories.length == 1 ? "One story" : `${stories.length} stories`} and ${games.length == 1 ? "one game" : `${games.length} games`} tagged with "${tag}".`; + totalWorksWithTag = `${stories.length == 1 ? "One story" : `${stories.length} stories`} and ${games.length == 1 ? "one game" : `${games.length} games`} tagged with "${tag}".`; } --- <GalleryLayout pageTitle={`Works tagged "${tag}"`}> - <meta slot="head-description" content={`Bad Manners || ${tagDescription || tag}`} property="og:description" /> + <meta slot="head-description" content={`Bad Manners || ${totalWorksWithTag || tag}`} property="og:description" /> <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{tag}"</h1> - {tagDescription ? <p class="my-4">{tagDescription}</p> : null} + <div class="my-4"> + <Prose> + {description ? <Markdown of={description} /> : null} + { + related?.length ? ( + <p + set:html={`See also: ${related.map((relatedTag) => `<a href="/tags/${slug(relatedTag)}">${relatedTag}</a>`).join(", ")}.`} + /> + ) : null + } + <p>{totalWorksWithTag}</p> + </Prose> + </div> { stories.length > 0 && ( <section class="my-2" aria-labelledby="content-stories"> @@ -80,7 +129,11 @@ if (count == 1) { <ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal"> {stories.map((story) => ( <li class="break-inside-avoid"> - <a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}> + <a + class="text-link hover:underline focus:underline" + href={`/stories/${story.slug}`} + title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())} + > {story.data.thumbnail ? ( <div class="flex aspect-square max-w-[192px] justify-center"> <Image @@ -108,7 +161,11 @@ if (count == 1) { <ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal"> {games.map((game) => ( <li class="break-inside-avoid"> - <a class="text-link hover:underline focus:underline" href={`/games/${game.slug}`}> + <a + class="text-link hover:underline focus:underline" + href={`/games/${game.slug}`} + title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim())} + > {game.data.thumbnail ? ( <div class="flex aspect-[630/500] max-w-[192px] justify-center"> <Image diff --git a/src/utils/markdown_to_bbcode.ts b/src/utils/markdown_to_bbcode.ts new file mode 100644 index 0000000..b7e6c82 --- /dev/null +++ b/src/utils/markdown_to_bbcode.ts @@ -0,0 +1,33 @@ +import { Marked, type RendererApi } from "marked"; +import { decode } from "tiny-decode"; + +const renderer: RendererApi = { + strong: (text) => `[b]${text}[/b]`, + em: (text) => `[i]${text}[/i]`, + del: (text) => `[s]${text}[/s]`, + codespan: (code) => code, + br: () => `\n\n`, + link: (href, _, text) => `[url=${href}]${text}[/url]`, + image: (href) => `[img]${href}[/img]`, + text: (text) => text, + paragraph: (text) => `\n${text}\n`, + list: (body, ordered) => (ordered ? `\n[ol]\n${body}[/ol]\n` : `\n[ul]\n${body}[/ul]\n`), + listitem: (text) => `[li]${text}[/li]\n`, + blockquote: (quote) => `\n[quote]${quote}[/quote]\n`, + code: (code) => `\n[code]${code}[/code]\n`, + heading: (heading) => `\n${heading}\n`, + table: (header, body) => `\n[table]\n${header}${body}[/table]\n`, + tablerow: (content) => `[tr]\n${content}[/tr]\n`, + tablecell: (content, { header }) => (header ? `[th]${content}[/th]\n` : `[td]${content}[/td]\n`), + hr: () => `\n===\n`, + html: () => { + throw new Error("Not supported by BBCode: html"); + }, + checkbox: () => { + throw new Error("Not supported by BBCode: checkbox"); + }, +}; + +const bbcodeRenderer = new Marked({ renderer, async: false }); + +export const markdownToBbcode = (text: string) => decode((bbcodeRenderer.parse(text) as string).trim()); diff --git a/src/utils/markdown_to_plaintext.ts b/src/utils/markdown_to_plaintext.ts new file mode 100644 index 0000000..51d4335 --- /dev/null +++ b/src/utils/markdown_to_plaintext.ts @@ -0,0 +1,35 @@ +import { Marked, type RendererApi } from "marked"; +import { decode } from "tiny-decode"; + +const renderer: RendererApi = { + strong: (text) => text, + em: (text) => `_${text}_`, + del: (text) => text, + codespan: (code) => code, + br: () => `\n\n`, + link: (_href, _title, text) => text, + text: (text) => text, + paragraph: (text) => `\n${text}\n`, + list: (body) => `\n${body}\n`, + listitem: (text) => `- ${text}\n`, + blockquote: (quote) => `\n> ${quote}\n`, + code: (code) => `\n${code}\n`, + heading: (heading) => `\n${heading}\n`, + table: (header, body) => `\n${header}\n---\n${body}\n`, + tablerow: (content) => `${content.slice(0, -3)}\n`, + tablecell: (content) => `${content} | `, + hr: () => `\n***\n`, + image: () => { + throw new Error("Not supported by plaintext: img"); + }, + html: () => { + throw new Error("Not supported by plaintext: html"); + }, + checkbox: () => { + throw new Error("Not supported by plaintext: checkbox"); + }, +}; + +const plaintextRenderer = new Marked({ renderer, async: false }); + +export const markdownToPlaintext = (text: string) => decode((plaintextRenderer.parse(text) as string).trim()); diff --git a/tailwind.config.mjs b/tailwind.config.mjs index c50aaf6..41dc640 100644 --- a/tailwind.config.mjs +++ b/tailwind.config.mjs @@ -22,7 +22,7 @@ export default { radial: "radial-gradient(var(--tw-gradient-stops))", }, typography: ({ theme }) => ({ - story: { + bm: { css: { "--tw-prose-body": theme("colors.stone[800]"), "--tw-prose-headings": theme("colors.stone[900]"),