mirror of
https://github.com/kogakure/website-astro-stefanimhoff.de.git
synced 2026-02-03 12:05:28 +00:00
feat: replace RSS handling
This replaces the RSS hack with a custom helper that replaces MDX components manually.
This commit is contained in:
committed by
Stefan Imhoff
parent
f4e41cb807
commit
ceadc30005
@@ -63,6 +63,7 @@
|
||||
"jimp": "^0.22.12",
|
||||
"lint-staged": "^15.2.4",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pagefind": "^1.1.0",
|
||||
"plop": "^4.0.1",
|
||||
@@ -74,6 +75,7 @@
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"rollup": "^4.18.0",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"sharp": "^0.33.4",
|
||||
"svgo": "^3.3.2",
|
||||
"tailwindcss-logical": "^3.0.1",
|
||||
|
||||
69
pnpm-lock.yaml
generated
69
pnpm-lock.yaml
generated
@@ -151,6 +151,9 @@ importers:
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
markdown-it:
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0
|
||||
npm-run-all:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5
|
||||
@@ -184,6 +187,9 @@ importers:
|
||||
rollup:
|
||||
specifier: npm:@rollup/wasm-node
|
||||
version: '@rollup/wasm-node@4.55.2'
|
||||
sanitize-html:
|
||||
specifier: ^2.14.0
|
||||
version: 2.17.0
|
||||
sharp:
|
||||
specifier: 0.33.4
|
||||
version: 0.33.4
|
||||
@@ -3194,6 +3200,9 @@ packages:
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
htmlparser2@8.0.2:
|
||||
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||
|
||||
http-cache-semantics@4.1.1:
|
||||
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
|
||||
|
||||
@@ -3661,6 +3670,9 @@ packages:
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
linkify-it@5.0.0:
|
||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||
|
||||
lint-staged@15.2.9:
|
||||
resolution: {integrity: sha512-BZAt8Lk3sEnxw7tfxM7jeZlPRuT4M68O0/CwZhhaw6eeWu0Lz5eERE3m386InivXB64fp/mDID452h48tvKlRQ==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
@@ -3767,6 +3779,10 @@ packages:
|
||||
resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||
hasBin: true
|
||||
|
||||
markdown-table@3.0.3:
|
||||
resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
|
||||
|
||||
@@ -3827,6 +3843,9 @@ packages:
|
||||
mdn-data@2.0.30:
|
||||
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
||||
|
||||
mdurl@2.0.0:
|
||||
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||
|
||||
memorystream@0.3.1:
|
||||
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
@@ -4254,6 +4273,9 @@ packages:
|
||||
resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
parse-srcset@1.0.2:
|
||||
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
|
||||
|
||||
parse5@7.1.2:
|
||||
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
|
||||
|
||||
@@ -4773,6 +4795,10 @@ packages:
|
||||
property-information@6.5.0:
|
||||
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
|
||||
|
||||
punycode.js@2.3.1:
|
||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -4990,6 +5016,9 @@ packages:
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
sanitize-html@2.17.0:
|
||||
resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==}
|
||||
|
||||
sass-formatter@0.7.9:
|
||||
resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==}
|
||||
|
||||
@@ -5442,6 +5471,9 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
uc.micro@2.1.0:
|
||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||
|
||||
uglify-js@3.19.2:
|
||||
resolution: {integrity: sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
@@ -9723,6 +9755,13 @@ snapshots:
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
htmlparser2@8.0.2:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.1.0
|
||||
entities: 4.5.0
|
||||
|
||||
http-cache-semantics@4.1.1: {}
|
||||
|
||||
human-signals@5.0.0: {}
|
||||
@@ -10139,6 +10178,10 @@ snapshots:
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
linkify-it@5.0.0:
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
|
||||
lint-staged@15.2.9:
|
||||
dependencies:
|
||||
chalk: 5.3.0
|
||||
@@ -10270,6 +10313,15 @@ snapshots:
|
||||
|
||||
markdown-extensions@2.0.0: {}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
entities: 4.5.0
|
||||
linkify-it: 5.0.0
|
||||
mdurl: 2.0.0
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 2.1.0
|
||||
|
||||
markdown-table@3.0.3: {}
|
||||
|
||||
mdast-util-definitions@6.0.0:
|
||||
@@ -10445,6 +10497,8 @@ snapshots:
|
||||
|
||||
mdn-data@2.0.30: {}
|
||||
|
||||
mdurl@2.0.0: {}
|
||||
|
||||
memorystream@0.3.1: {}
|
||||
|
||||
merge-stream@2.0.0: {}
|
||||
@@ -11061,6 +11115,8 @@ snapshots:
|
||||
|
||||
parse-passwd@1.0.0: {}
|
||||
|
||||
parse-srcset@1.0.2: {}
|
||||
|
||||
parse5@7.1.2:
|
||||
dependencies:
|
||||
entities: 4.5.0
|
||||
@@ -11462,6 +11518,8 @@ snapshots:
|
||||
|
||||
property-information@6.5.0: {}
|
||||
|
||||
punycode.js@2.3.1: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
@@ -11742,6 +11800,15 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sanitize-html@2.17.0:
|
||||
dependencies:
|
||||
deepmerge: 4.3.1
|
||||
escape-string-regexp: 4.0.0
|
||||
htmlparser2: 8.0.2
|
||||
is-plain-object: 5.0.0
|
||||
parse-srcset: 1.0.2
|
||||
postcss: 8.4.41
|
||||
|
||||
sass-formatter@0.7.9:
|
||||
dependencies:
|
||||
suf-log: 2.5.3
|
||||
@@ -12289,6 +12356,8 @@ snapshots:
|
||||
|
||||
typescript@5.5.4: {}
|
||||
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
uglify-js@3.19.2:
|
||||
optional: true
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,10 +16,10 @@ const { class: className, id, ...props } = Astro.props;
|
||||
className,
|
||||
]}
|
||||
data-umami-event={`Apple TV+: ${id}`}
|
||||
href={`https://tv.apple.com/show/foundation/umc.cmc.${id}`}
|
||||
href={`https://tv.apple.com/show/umc.cmc.${id}`}
|
||||
title="Apple TV+"
|
||||
{...props}
|
||||
><span class="hidden" aria-hidden="true">[</span><span class="hidden" aria-hidden="true"
|
||||
>etflix]</span
|
||||
>]</span
|
||||
></Link
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ const { class: className, noMargin, src, ...props } = Astro.props;
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
'image-shadow block h-auto w-full rounded-1 border-1 border-solid border-black/[0.1] bg-black/[0.1] shadow shadow-black/10 mbe-10 mbs-0 dark:border-white/[0.1] dark:opacity-[0.87] dark:shadow-white/10',
|
||||
'block h-auto w-full rounded-2 mbe-10 mbs-0 dark:opacity-[0.87]',
|
||||
{ 'mbe-0': noMargin },
|
||||
className,
|
||||
]}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
export interface Props {
|
||||
class?: string;
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const { class: className, id, ...props } = Astro.props;
|
||||
---
|
||||
|
||||
<div class:list={['relative aspect-video mbe-10', className]} {...props}>
|
||||
<iframe
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
class="absolute h-full w-full"
|
||||
frameborder="0"
|
||||
src={`https://odysee.com/$/embed/${id}`}></iframe>
|
||||
</div>
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
import { site } from '../data/site';
|
||||
import { dateToISO } from '../utils';
|
||||
|
||||
import { rssMapping } from '../mdx-components';
|
||||
|
||||
const { allPosts } = Astro.props;
|
||||
const rssHeaderXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
<channel>
|
||||
<title>${site.title}</title>
|
||||
<description><![CDATA[ ${site.description} ]]></description>
|
||||
<link>${site.url}</link>`;
|
||||
|
||||
const rssFooterXml = ` </channel>
|
||||
</rss>`;
|
||||
---
|
||||
|
||||
<Fragment set:html={rssHeaderXml} />
|
||||
{
|
||||
allPosts
|
||||
.filter((post: any) => !post.frontmatter.draft)
|
||||
.map((post: any) => (
|
||||
<>
|
||||
<Fragment
|
||||
set:html={` <item>
|
||||
<title>${post.frontmatter.title.replace('&', '&')}</title>
|
||||
<link>${`${site.url}/${post.frontmatter.slug}/`}</link>
|
||||
<guid>${`${site.url}/${post.frontmatter.slug}/`}</guid>
|
||||
<description><![CDATA[${post.frontmatter.description}]]></description>
|
||||
<pubDate>${dateToISO(post.frontmatter.date)}</pubDate>
|
||||
<enclosure url="${
|
||||
site.url +
|
||||
(post.frontmatter.cover
|
||||
? post.frontmatter.cover.startsWith('/assets/images/cover/') &&
|
||||
post.frontmatter.cover.endsWith('.webp')
|
||||
? post.frontmatter.cover
|
||||
.replace('/assets/images/cover/', '/assets/images/og/')
|
||||
.replace(/\.webp$/, '.jpg')
|
||||
: post.frontmatter.cover
|
||||
: '/assets/images/og/bonsai.jpg')
|
||||
}" length="0" type="image/jpeg" />
|
||||
<content:encoded><![CDATA[`}
|
||||
/>
|
||||
<post.Content components={rssMapping} />
|
||||
<Fragment
|
||||
set:html={`]]></content:encoded>
|
||||
</item>
|
||||
`}
|
||||
/>
|
||||
</>
|
||||
))
|
||||
}
|
||||
<Fragment set:html={rssFooterXml} />
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
const distFolder = './dist';
|
||||
const filename = 'rss.xml';
|
||||
const fileUrl = path.join(distFolder, filename);
|
||||
|
||||
if (Astro.slots.has('rss-writer')) {
|
||||
const html = await Astro.slots.render('rss-writer');
|
||||
await fs.writeFile(fileUrl, html);
|
||||
}
|
||||
---
|
||||
@@ -12,7 +12,7 @@ tags: ["productivity", "self-improvement", "book", "minimalism"]
|
||||
|
||||
For one and a half years I use now the principles of _GTD_ (<em>Getting Things Done</em>®), from the book by _David Allen_, to organize my tasks.
|
||||
|
||||
In his book _<ProductLink asin="0143126563" text="Getting Things Done: The Art of Stress-Free Productivity" />_, David Allen introduces an interesting system that allows you to do your tasks efficiently.
|
||||
In his book <ProductLink asin="0143126563" text="Getting Things Done: The Art of Stress-Free Productivity" />, David Allen introduces an interesting system that allows you to do your tasks efficiently.
|
||||
|
||||
<Bookshelf>
|
||||
<AmazonBook asin="0143126563" alt="Getting Things Done: The Art of Stress-Free Productivity" />
|
||||
|
||||
@@ -10,7 +10,7 @@ tags: ["design", "download", "book", "japan", "recommendation"]
|
||||
|
||||
During a visit to the art exhibition _Japan and the West_ in the [Art Museum Wolfsburg](https://www.kunstmuseum-wolfsburg.de/), I was able to get my hands on a wonderful book about the traditional, Japanese colors.
|
||||
|
||||
In the museum shop, I bought the book _<ProductLink asin="4894445786" text="The Traditional Colors of Japan" />_ which is in Japanese. In addition to a full-color palette of 250 colors, it contains an exact indication of RGB, CMYK, and other information.
|
||||
In the museum shop, I bought the book <ProductLink asin="4894445786" text="The Traditional Colors of Japan" /> which is in Japanese. In addition to a full-color palette of 250 colors, it contains an exact indication of RGB, CMYK, and other information.
|
||||
|
||||
<Bookshelf>
|
||||
<AmazonBook asin="4894445786" alt="The Traditional Colors of Japan" />
|
||||
@@ -19,7 +19,7 @@ In the museum shop, I bought the book _<ProductLink asin="4894445786" text="The
|
||||
|
||||
Each page of the book shows three colors that are always juxtaposed with one or more stunning photos. The photos show the traditional use of colors in kimonos, samurai armor, monk clothing, or nature.
|
||||
|
||||
In 2011 the second book, _<ProductLink asin="475624114X" text="Traditional Japanese Color Palette" />_ was released which showcases a huge amount of Japanese color palattes with examples from culture, tradition, and art.
|
||||
In 2011 the second book, <ProductLink asin="475624114X" text="Traditional Japanese Color Palette" /> was released which showcases a huge amount of Japanese color palattes with examples from culture, tradition, and art.
|
||||
|
||||
## Color Palette for Graphic Software
|
||||
|
||||
|
||||
@@ -21,10 +21,7 @@ These works of art are painful (dozens of bamboo needles are [stung several time
|
||||
One of the best living artists of the Irezumi is Horiyoshi III, whose works can be seen in many illustrated books: <ProductLink asin="9074822452" text="Tattoos from the Floating World: Ukiyo-e Motif’s in Japanese Tattoo" /> and <ProductLink asin="0764312014" text="Bushido: Legacies of the Japanese Tattoo" />.
|
||||
|
||||
<Bookshelf>
|
||||
<AmazonBook
|
||||
asin="9074822452"
|
||||
alt="Tattoos from the Floating World: Ukiyo-e Motifs in the Japanese Tattoo"
|
||||
/>
|
||||
<AmazonBook asin="9074822452" alt="Tattoos from the Floating World: Ukiyo-e Motifs in the Japanese Tattoo" />
|
||||
<AmazonBook asin="0764312014" alt="Bushido" />
|
||||
</Bookshelf>
|
||||
|
||||
@@ -38,7 +35,7 @@ The links to the Japanese art of [ukiyo-e](https://grokipedia.com/page/Ukiyo-e)
|
||||
|
||||
## Motives
|
||||
|
||||
In addition to gods, mythical creatures, and demons, the most important source of motifs is the ancient Chinese novella _[Shuǐhǔ Zhuàn](https://grokipedia.com/page/Water_Margin)_ (Water Margin), known in Japan as _Suikoden_. It belongs to one of the four classical books of Chinese literature. The German translation of this entertaining book is called _<ProductLink asin="3458318917" text="Die Räuber vom Liang Schan Moor" />_.
|
||||
In addition to gods, mythical creatures, and demons, the most important source of motifs is the ancient Chinese novella _[Shuǐhǔ Zhuàn](https://grokipedia.com/page/Water_Margin)_ (Water Margin), known in Japan as _Suikoden_. It belongs to one of the four classical books of Chinese literature. The German translation of this entertaining book is called <ProductLink asin="3458318917" text="Die Räuber vom Liang Schan Moor" />.
|
||||
|
||||
## Water Margin
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import ListItem from './components/ListItem.astro';
|
||||
import MarkdownImage from './components/MarkdownImage.astro';
|
||||
import MoreLink from './components/MoreLink.astro';
|
||||
import NetflixFlag from './components/NetflixFlag.astro';
|
||||
import OdyseeVideo from './components/OdyseeVideo.astro';
|
||||
import OrderedList from './components/OrderedList.astro';
|
||||
import PrimeVideoFlag from './components/PrimeVideoFlag.astro';
|
||||
import ProductLink from './components/ProductLink.astro';
|
||||
@@ -55,7 +54,6 @@ export const mapping = {
|
||||
MarkdownImage,
|
||||
MoreLink,
|
||||
NetflixFlag,
|
||||
OdyseeVideo,
|
||||
PrimeVideoFlag,
|
||||
ProductLink,
|
||||
ProjectIntro,
|
||||
@@ -80,34 +78,3 @@ export const mapping = {
|
||||
p: Text,
|
||||
ul: UnorderedList,
|
||||
};
|
||||
|
||||
// Mapping for RSS feed to reduce the size of the feed
|
||||
export const rssMapping = {
|
||||
AmazonBook,
|
||||
AppleTVFlag,
|
||||
Banner,
|
||||
Blockquote,
|
||||
Book,
|
||||
Bookshelf,
|
||||
ColorStack,
|
||||
ColorSwatch,
|
||||
DisplayBox,
|
||||
DownloadLink,
|
||||
EmailLink,
|
||||
Figure,
|
||||
Flag,
|
||||
Image,
|
||||
MarkdownImage,
|
||||
MoreLink,
|
||||
NetflixFlag,
|
||||
OdyseeVideo,
|
||||
PrimeVideoFlag,
|
||||
ProductLink,
|
||||
ProjectIntro,
|
||||
Pullquote,
|
||||
Ruby,
|
||||
Spotify,
|
||||
ThemeBox,
|
||||
Verse,
|
||||
YouTube,
|
||||
};
|
||||
|
||||
@@ -13,13 +13,6 @@ import PageTitle from '../components/PageTitle.astro';
|
||||
import Image from '../components/Image.astro';
|
||||
import JournalList from '../components/JournalList.astro';
|
||||
|
||||
/* FIXME: Remove hack as soon as this issue is resolved:
|
||||
* Issue: https://github.com/withastro/roadmap/issues/533
|
||||
* Proposal: https://github.com/withastro/roadmap/discussions/419
|
||||
*/
|
||||
import WriteFile from '../components/WriteFile.astro';
|
||||
import RssXml from '../components/RssXml.astro';
|
||||
|
||||
import Headline from '../components/Headline.astro';
|
||||
import MoreLink from '../components/MoreLink.astro';
|
||||
|
||||
@@ -92,7 +85,3 @@ rssPosts.sort(sortMarkdownByDate);
|
||||
<JournalList entries={formattedLatest} />
|
||||
</article>
|
||||
</GridLayout>
|
||||
|
||||
<WriteFile slot="rss-writer">
|
||||
<RssXml allPosts={isProduction ? rssPosts : []} slot="rss-writer" />
|
||||
</WriteFile>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getCollection } from 'astro:content';
|
||||
import { site } from '../data/site';
|
||||
import { sortByDate } from '../utils';
|
||||
|
||||
export async function get(context) {
|
||||
export async function GET(context) {
|
||||
const haiku = await getCollection('haiku');
|
||||
haiku.sort(sortByDate);
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function get(context) {
|
||||
pubDate: item.data.date,
|
||||
customData: '<language>en-us</language>',
|
||||
link: `/haiku/${item.slug}/`,
|
||||
content: `<div><p>${item.data.de}</p><hr /><p>${item.data.en}</p></div>`,
|
||||
content: `<blockquote><p>${item.data.de}</p><hr /><p>${item.data.en}</p></blockquote>`,
|
||||
})),
|
||||
customData: `<language>en-us</language>`,
|
||||
});
|
||||
|
||||
77
src/pages/rss.xml.js
Normal file
77
src/pages/rss.xml.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
import { site } from '../data/site';
|
||||
import { sortByDate } from '../utils';
|
||||
|
||||
const parser = new MarkdownIt({ html: true });
|
||||
|
||||
import { stripMDXComponents } from '../utils';
|
||||
|
||||
export async function GET(context) {
|
||||
const journal = await getCollection('journal', ({ data }) => !data.draft);
|
||||
const haiku = await getCollection('haiku');
|
||||
journal.sort(sortByDate);
|
||||
haiku.sort(sortByDate);
|
||||
|
||||
return rss({
|
||||
stylesheet: '/rss.xsl',
|
||||
title: site.title,
|
||||
description: site.description,
|
||||
site: context.site,
|
||||
items: [
|
||||
...journal.map((post) => {
|
||||
// Filter out import statements from content
|
||||
const contentWithoutImports = post.body
|
||||
.split('\n')
|
||||
.filter((line) => !line.startsWith('import'))
|
||||
.join('\n');
|
||||
|
||||
// First strip MDX components, then render markdown
|
||||
const processedContent = stripMDXComponents(contentWithoutImports, context.site);
|
||||
const renderedContent = parser.render(processedContent);
|
||||
const sanitizedContent = sanitizeHtml(renderedContent, {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat([
|
||||
'img',
|
||||
'figure',
|
||||
'figcaption',
|
||||
'details',
|
||||
'summary',
|
||||
'div',
|
||||
]),
|
||||
allowedAttributes: {
|
||||
div: ['style'],
|
||||
a: ['href'],
|
||||
img: ['src', 'alt', 'width', 'height'],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
title: post.data.title,
|
||||
pubDate: post.data.date,
|
||||
description: `<![CDATA[${post.data.description}]]>`,
|
||||
link: `/${post.slug}/`,
|
||||
content: `<![CDATA[${sanitizedContent}]]>`,
|
||||
enclosure: {
|
||||
url: `${site.url}${post.data.cover.startsWith('/assets/images/cover/') && post.data.cover.endsWith('.webp') ? post.data.cover.replace('/assets/images/cover/', '/assets/images/og/').replace(/\.webp$/, '.jpg') : '/assets/images/og/bonsai.jpg'}`,
|
||||
length: 0,
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
customData: '<language>en-us</language>',
|
||||
};
|
||||
}),
|
||||
...haiku.map((item) => {
|
||||
return {
|
||||
title: `Haiku ${item.slug}`,
|
||||
pubDate: item.data.date,
|
||||
customData: '<language>en-us</language>',
|
||||
link: `/haiku/${item.slug}/`,
|
||||
content: `<blockquote><p>${item.data.de}</p><hr /><p>${item.data.en}</p></blockquote>`,
|
||||
};
|
||||
}),
|
||||
].sort((a, b) => b.pubDate.valueOf() - a.pubDate.valueOf()),
|
||||
customData: `<language>en-us</language>`,
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export * from './is-production';
|
||||
export * from './pick-two-random-colors';
|
||||
export * from './remark-reading-time';
|
||||
export * from './remark-widont';
|
||||
export * from './rss-parser';
|
||||
export * from './sort-by-alphabet';
|
||||
export * from './sort-by-date';
|
||||
export * from './sort-by-sortkey';
|
||||
|
||||
510
src/utils/rss-parser.ts
Normal file
510
src/utils/rss-parser.ts
Normal file
@@ -0,0 +1,510 @@
|
||||
/**
|
||||
* Helper utilities for RSS parsing and content normalization.
|
||||
*
|
||||
* Extracted from src/pages/rss-test.xml.js to centralize helpers.
|
||||
*
|
||||
* Functions:
|
||||
* - stripMarkdown(text): Remove simple markdown formatting.
|
||||
* - makeAbsolute(url, siteUrl): Convert relative URLs to absolute using siteUrl.
|
||||
* - fixImagePaths(html, siteUrl): Replace <img src="..."> with absolute src.
|
||||
* - replaceImageComponent(attributes, siteUrl): Convert an MDX `<Image ... />` component into HTML.
|
||||
* - replaceAmazonBookComponent(attributes): Convert an MDX `<AmazonBook ... />` component into HTML.
|
||||
* - stripMDXComponents(text, siteUrl): Replace `<Image />`, `<AmazonBook />` and strip other MDX component tags.
|
||||
*
|
||||
* These are implemented in TypeScript with minimal dependencies so they can be
|
||||
* used from RSS builder code or other places.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
export function stripMarkdown(text: string): string {
|
||||
// Remove markdown links: [text](url) => text
|
||||
// Remove basic markdown formatting characters: *, _, `, ~
|
||||
return text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1').replace(/[*_`~]/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a possibly-relative url to an absolute URL using siteUrl as the base.
|
||||
* If the url is already absolute (http/https) it is returned unchanged.
|
||||
*/
|
||||
export function makeAbsolute(url: string, siteUrl: string): string {
|
||||
if (!url) return url;
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
try {
|
||||
// The URL constructor will resolve relative URLs against the base.
|
||||
return new URL(url, siteUrl).toString();
|
||||
} catch (e) {
|
||||
// Fallback: simple concatenation with a single slash between.
|
||||
const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;
|
||||
const path = url.startsWith('/') ? url : `/${url}`;
|
||||
return `${base}${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace img tags in HTML that have relative src attributes with absolute URLs.
|
||||
* This is useful after rendering markdown/html that contains <img src="...">.
|
||||
*/
|
||||
export function fixImagePaths(html: string, siteUrl: string): string {
|
||||
if (!html) return html;
|
||||
return html.replace(
|
||||
/<img([^>]*)\s+src=(?:"|')([^"']*)(?:"|')([^>]*)>/g,
|
||||
(_match, beforeSrc: string, src: string, afterSrc: string) => {
|
||||
const absoluteSrc = makeAbsolute(src, siteUrl);
|
||||
return `<img${beforeSrc} src="${absoluteSrc}"${afterSrc}>`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract attribute value from a string like `src="value"` or `src='value'`.
|
||||
* Returns null if attribute is not found.
|
||||
*/
|
||||
function getAttr(str: string, name: string): string | null {
|
||||
const regex = new RegExp(`${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, 'i');
|
||||
const match = str.match(regex);
|
||||
return match ? match[1] || match[2] : null;
|
||||
}
|
||||
|
||||
// Convert an MDX <Image ... />
|
||||
export function replaceImageComponent(attributes: string, siteUrl: string): string {
|
||||
// Extract possible props we care about
|
||||
const src = getAttr(attributes, 'src');
|
||||
const alt = getAttr(attributes, 'alt') || '';
|
||||
const caption = getAttr(attributes, 'caption');
|
||||
const source = getAttr(attributes, 'source');
|
||||
const sourceUrl = getAttr(attributes, 'sourceUrl');
|
||||
const href = getAttr(attributes, 'href');
|
||||
const width = getAttr(attributes, 'width');
|
||||
const height = getAttr(attributes, 'height');
|
||||
|
||||
if (!src) return '';
|
||||
|
||||
const absoluteSrc = makeAbsolute(src, siteUrl);
|
||||
|
||||
// Build <img> tag
|
||||
let imgHtml = `<img src="${absoluteSrc}" alt="${escapeHtmlAttr(alt)}"`;
|
||||
if (width) imgHtml += ` width="${escapeHtmlAttr(width)}"`;
|
||||
if (height) imgHtml += ` height="${escapeHtmlAttr(height)}"`;
|
||||
imgHtml += ` />`;
|
||||
|
||||
// Optionally wrap in <a>
|
||||
if (href) {
|
||||
const safeHref = escapeHtmlAttr(href);
|
||||
imgHtml = `<a href="${safeHref}">${imgHtml}</a>`;
|
||||
}
|
||||
|
||||
// Build figcaption if needed
|
||||
if (caption || source) {
|
||||
let captionContent = '';
|
||||
|
||||
if (caption) captionContent += escapeHtml(caption);
|
||||
|
||||
if (caption && source) captionContent += ' – ';
|
||||
|
||||
if (source) {
|
||||
if (sourceUrl) {
|
||||
captionContent += `<cite><a href="${escapeHtmlAttr(sourceUrl)}">${escapeHtml(source)}</a></cite>`;
|
||||
} else {
|
||||
captionContent += `<cite>${escapeHtml(source)}</cite>`;
|
||||
}
|
||||
}
|
||||
|
||||
return `<figure>${imgHtml}<figcaption>${captionContent}</figcaption></figure>`;
|
||||
}
|
||||
|
||||
return `<figure>${imgHtml}</figure>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <AmazonBook ... />
|
||||
export function replaceAmazonBookComponent(attributes: string): string {
|
||||
const asin = getAttr(attributes, 'asin');
|
||||
const alt = getAttr(attributes, 'alt') || '';
|
||||
|
||||
if (!asin) return '';
|
||||
|
||||
// Construct Amazon Image URL
|
||||
const amazonImageUrl = `https://images-na.ssl-images-amazon.com/images/P/${asin}.01.LZZZZZZZ.jpg`;
|
||||
const amazonProductUrl = `https://www.amazon.de/gp/product/${asin}`;
|
||||
|
||||
// We simplify the output to a standard anchor + img tag
|
||||
return `<a href="${amazonProductUrl}" aria-label="${escapeHtmlAttr(alt)}">
|
||||
<img src="${amazonImageUrl}" alt="${escapeHtmlAttr(alt)}" />
|
||||
</a>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <AppleTV ... />
|
||||
export function replaceAppleTvComponent(attributes: string): string {
|
||||
const id = getAttr(attributes, 'id');
|
||||
|
||||
if (!id) return '';
|
||||
|
||||
// The URL pattern from the component
|
||||
const url = `https://tv.apple.com/show/umc.cmc.${id}`;
|
||||
|
||||
// We preserve the inner HTML structure (spans and Apple logo)
|
||||
// but strip the complex Tailwind classes for the "pure HTML" output.
|
||||
return `<a href="${url}" title="Apple TV+">[]</a>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <NetflixFlag ... />
|
||||
export function replaceNetflixComponent(attributes: string): string {
|
||||
const id = getAttr(attributes, 'id');
|
||||
|
||||
if (!id) return '';
|
||||
|
||||
// The URL pattern from the component
|
||||
const url = `https://www.netflix.com/title/${id}`;
|
||||
|
||||
// We preserve the inner HTML structure (spans and Apple logo)
|
||||
// but strip the complex Tailwind classes for the "pure HTML" output.
|
||||
return `<a href="${url}" title="Netflix">[Netflix]</a>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <PrimeVideoFlag ... />
|
||||
export function replacePrimeVideoComponent(attributes: string): string {
|
||||
const id = getAttr(attributes, 'id');
|
||||
|
||||
if (!id) return '';
|
||||
|
||||
// The URL pattern from the component
|
||||
const url = `https://www.amazon.de/gp/video/detail/${id}`;
|
||||
|
||||
// We preserve the inner HTML structure (spans and Apple logo)
|
||||
// but strip the complex Tailwind classes for the "pure HTML" output.
|
||||
return `<a href="${url}" title="Prime Video">[Prime Video]</a>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <Flag ... />
|
||||
export function replaceFlagComponent(attributes: string, siteUrl: string): string {
|
||||
const label = getAttr(attributes, 'label');
|
||||
const href = getAttr(attributes, 'href');
|
||||
|
||||
if (!label) return '';
|
||||
|
||||
// Inner content with decorative brackets, mimicking the original component
|
||||
const innerHtml = `[${escapeHtml(label)}]`;
|
||||
|
||||
if (href) {
|
||||
const absoluteHref = makeAbsolute(href, siteUrl);
|
||||
return `<a href="${escapeHtmlAttr(absoluteHref)}" title="${escapeHtmlAttr(label)}">${innerHtml}</a>`;
|
||||
}
|
||||
|
||||
return `<span title="${escapeHtmlAttr(label)}">${innerHtml}</span>`;
|
||||
}
|
||||
|
||||
export function replaceBlockquoteComponent(
|
||||
attributes: string,
|
||||
content: string,
|
||||
siteUrl: string
|
||||
): string {
|
||||
const author = getAttr(attributes, 'author');
|
||||
const source = getAttr(attributes, 'source');
|
||||
const sourceUrl = getAttr(attributes, 'sourceUrl');
|
||||
const lang = getAttr(attributes, 'lang') || 'en';
|
||||
|
||||
let footerHtml = '';
|
||||
|
||||
// Build the footer if we have an author or source
|
||||
if (author || source) {
|
||||
footerHtml += '<footer>—';
|
||||
|
||||
if (author) {
|
||||
footerHtml += ` <b>${escapeHtml(author)}</b>`;
|
||||
}
|
||||
|
||||
if (author && source) {
|
||||
footerHtml += ',';
|
||||
}
|
||||
|
||||
if (source) {
|
||||
const safeSource = escapeHtml(source);
|
||||
// Add space before source
|
||||
footerHtml += ' ';
|
||||
|
||||
if (sourceUrl) {
|
||||
const absoluteUrl = makeAbsolute(sourceUrl, siteUrl);
|
||||
footerHtml += `<cite><a href="${escapeHtmlAttr(absoluteUrl)}">${safeSource}</a></cite>`;
|
||||
} else {
|
||||
footerHtml += `<cite>${safeSource}</cite>`;
|
||||
}
|
||||
}
|
||||
|
||||
footerHtml += '</footer>';
|
||||
}
|
||||
|
||||
return `<blockquote lang="${escapeHtmlAttr(lang)}">${content}${footerHtml}</blockquote>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <Pullquote ... />
|
||||
export function replacePullquoteComponent(attributes: string, siteUrl: string): string {
|
||||
const text = getAttr(attributes, 'text');
|
||||
if (!text) return '';
|
||||
|
||||
const author = getAttr(attributes, 'author');
|
||||
const source = getAttr(attributes, 'source');
|
||||
const sourceUrl = getAttr(attributes, 'sourceUrl');
|
||||
const lang = getAttr(attributes, 'lang') || 'en';
|
||||
const alignment = getAttr(attributes, 'alignment') || 'center';
|
||||
|
||||
// Map alignment to inline styles for RSS compatibility
|
||||
const style = alignment === 'left' ? 'text-align: left;' : 'text-align: center;';
|
||||
|
||||
let footerHtml = '';
|
||||
|
||||
if (author || source) {
|
||||
footerHtml += '<footer>';
|
||||
|
||||
if (author) {
|
||||
footerHtml += `<b>${escapeHtml(author)}</b>`;
|
||||
}
|
||||
|
||||
if (author && source) {
|
||||
footerHtml += ', ';
|
||||
}
|
||||
|
||||
if (source) {
|
||||
const safeSource = escapeHtml(source);
|
||||
if (sourceUrl) {
|
||||
const absoluteUrl = makeAbsolute(sourceUrl, siteUrl);
|
||||
footerHtml += `<cite><a href="${escapeHtmlAttr(absoluteUrl)}">${safeSource}</a></cite>`;
|
||||
} else {
|
||||
footerHtml += `<cite>${safeSource}</cite>`;
|
||||
}
|
||||
}
|
||||
|
||||
footerHtml += '</footer>';
|
||||
}
|
||||
|
||||
return `<blockquote lang="${escapeHtmlAttr(lang)}" style="${style}"><p>${text}</p>${footerHtml}</blockquote>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <ProductLink ... />
|
||||
export function replaceProductLinkComponent(attributes: string): string {
|
||||
const asin = getAttr(attributes, 'asin');
|
||||
const text = getAttr(attributes, 'text');
|
||||
|
||||
if (!asin || !text) return '';
|
||||
|
||||
const url = `https://www.amazon.de/gp/product/${asin}`;
|
||||
return `<a href="${url}">${escapeHtml(text)}</a>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <DownloadLink ... />
|
||||
export function replaceDownloadLinkComponent(attributes: string, siteUrl: string): string {
|
||||
const href = getAttr(attributes, 'href');
|
||||
const text = getAttr(attributes, 'text');
|
||||
|
||||
if (!href || !text) return '';
|
||||
|
||||
const absoluteHref = makeAbsolute(href, siteUrl);
|
||||
|
||||
return `<a href="${escapeHtmlAttr(absoluteHref)}">${escapeHtml(text)} ↓</a>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <MoreLink ... />
|
||||
export function replaceMoreLinkComponent(attributes: string, siteUrl: string): string {
|
||||
const href = getAttr(attributes, 'href');
|
||||
const text = getAttr(attributes, 'text');
|
||||
|
||||
if (!href || !text) return '';
|
||||
|
||||
const absoluteHref = makeAbsolute(href, siteUrl);
|
||||
|
||||
return `<a href="${escapeHtmlAttr(absoluteHref)}">${escapeHtml(text)} →</a>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <Ruby ... />
|
||||
export function replaceRubyComponent(attributes: string): string {
|
||||
const base = getAttr(attributes, 'base');
|
||||
const text = getAttr(attributes, 'text');
|
||||
|
||||
if (!base || !text) return '';
|
||||
|
||||
return `<ruby>${escapeHtml(base)}<rp>(</rp><rt>${escapeHtml(text)}</rt><rp>)</rp></ruby>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <Spotify ... />
|
||||
export function replaceSpotifyComponent(attributes: string): string {
|
||||
const id = getAttr(attributes, 'id');
|
||||
|
||||
if (!id) return '';
|
||||
|
||||
// Construct the Spotify embed URL
|
||||
const src = `https://open.spotify.com/embed/show/${id}?utm_source=generator&theme=0`;
|
||||
|
||||
return `<iframe src="${src}" width="100%" height="352" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>`;
|
||||
}
|
||||
|
||||
// Convert an MDX <Figure ...>...</Figure>
|
||||
export function replaceFigureComponent(attributes: string, content: string): string {
|
||||
const caption = getAttr(attributes, 'caption');
|
||||
|
||||
let html = `<figure>${content}`;
|
||||
|
||||
if (caption) {
|
||||
html += `<figcaption>${escapeHtml(caption)}</figcaption>`;
|
||||
}
|
||||
|
||||
html += `</figure>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
// Convert an MDX <Banner ...>...</Banner>
|
||||
export function replaceBannerComponent(attributes: string, content: string): string {
|
||||
const summary = getAttr(attributes, 'summary');
|
||||
// Check for the presence of the 'open' attribute (boolean or explicitly set)
|
||||
const isOpen = /\bopen\b/i.test(attributes);
|
||||
|
||||
let html = '<aside>';
|
||||
|
||||
if (summary) {
|
||||
html += `<details${isOpen ? ' open' : ''}><summary><strong>${escapeHtml(summary)}</strong></summary>${content}</details>`;
|
||||
} else {
|
||||
html += content;
|
||||
}
|
||||
|
||||
html += '</aside>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// Convert an MDX <ColorSwatch ... />
|
||||
export function replaceColorSwatchComponent(attributes: string): string {
|
||||
const color = getAttr(attributes, 'color');
|
||||
|
||||
if (!color) return '';
|
||||
|
||||
return `<div style="width: 100px; height: 100px; background-color: ${escapeHtmlAttr(color)};"></div>`;
|
||||
}
|
||||
|
||||
// Convert wrapper components (like <ColorStack> and <BookShelf>)
|
||||
export function replaceWrapperComponent(content: string): string {
|
||||
return `<div>${content}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip MDX/JSX-like components from text but preserve/convert specific components.
|
||||
*/
|
||||
export function stripMDXComponents(text: string, siteUrl: string): string {
|
||||
if (!text) return text;
|
||||
|
||||
// <Image ... />
|
||||
let processed = text.replace(/<Image([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replaceImageComponent(attributes, siteUrl)
|
||||
);
|
||||
|
||||
// AmazonBook ... />
|
||||
processed = processed.replace(/<AmazonBook([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replaceAmazonBookComponent(attributes)
|
||||
);
|
||||
|
||||
// AppleTV ... />
|
||||
processed = processed.replace(
|
||||
/<Apple(Tv|TV)([\s\S]*?)\/>/g,
|
||||
(_match, _tagSuffix, attributes: string) => replaceAppleTvComponent(attributes)
|
||||
);
|
||||
|
||||
// <Netflix ... />
|
||||
processed = processed.replace(/<Netflix([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replaceNetflixComponent(attributes)
|
||||
);
|
||||
|
||||
// <PrimeVideo ... />
|
||||
processed = processed.replace(/<PrimeVideo([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replacePrimeVideoComponent(attributes)
|
||||
);
|
||||
|
||||
// <Flag ... />
|
||||
processed = processed.replace(/<Flag([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replaceFlagComponent(attributes, siteUrl)
|
||||
);
|
||||
|
||||
// <Blockquote ...>...</Blockquote>
|
||||
processed = processed.replace(
|
||||
/<Blockquote\b([^>]*)>([\s\S]*?)<\/Blockquote>/g,
|
||||
(_match, attributes: string, content: string) =>
|
||||
replaceBlockquoteComponent(attributes, content, siteUrl)
|
||||
);
|
||||
|
||||
// <Pullquote ... />
|
||||
processed = processed.replace(/<Pullquote([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replacePullquoteComponent(attributes, siteUrl)
|
||||
);
|
||||
|
||||
// <Figure ...>...</Figure>
|
||||
processed = processed.replace(
|
||||
/<Figure\b([^>]*)>([\s\S]*?)<\/Figure>/g,
|
||||
(_match, attributes: string, content: string) => replaceFigureComponent(attributes, content)
|
||||
);
|
||||
|
||||
// <Banner ...>...</Banner>
|
||||
processed = processed.replace(
|
||||
/<Banner\b([^>]*)>([\s\S]*?)<\/Banner>/g,
|
||||
(_match, attributes: string, content: string) => replaceBannerComponent(attributes, content)
|
||||
);
|
||||
|
||||
// <ProductLink ... />
|
||||
processed = processed.replace(/<ProductLink([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replaceProductLinkComponent(attributes)
|
||||
);
|
||||
|
||||
// <DownloadLink ... />
|
||||
processed = processed.replace(/<DownloadLink([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replaceDownloadLinkComponent(attributes, siteUrl)
|
||||
);
|
||||
|
||||
// <MoreLink ... />
|
||||
processed = processed.replace(/<MoreLink([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replaceMoreLinkComponent(attributes, siteUrl)
|
||||
);
|
||||
|
||||
// <Ruby ... />
|
||||
processed = processed.replace(/<Ruby([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replaceRubyComponent(attributes)
|
||||
);
|
||||
|
||||
// <Spotify ... />
|
||||
processed = processed.replace(/<Spotify([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replaceSpotifyComponent(attributes)
|
||||
);
|
||||
|
||||
// <ColorSwatch ... />
|
||||
processed = processed.replace(/<ColorSwatch([\s\S]*?)\/>/g, (_match, attributes: string) =>
|
||||
replaceColorSwatchComponent(attributes)
|
||||
);
|
||||
|
||||
// <ColorStack>...</ColorStack> and <BookShelf>...</BookShelf>
|
||||
processed = processed.replace(
|
||||
/<(ColorStack|Bookshelf)\b[^>]*>([\s\S]*?)<\/\1>/g,
|
||||
(_match, _tag, content: string) => replaceWrapperComponent(content)
|
||||
);
|
||||
|
||||
// Remove any other self-closing components e.g. <Foo bar="baz" />
|
||||
const removedSelfClosing = processed.replace(/<([A-Z][\w\d]*)\b[^>]*?\/>/g, '');
|
||||
|
||||
// Remove paired component tags, including their content, e.g. <Foo>...</Foo>
|
||||
const removedPaired = removedSelfClosing.replace(
|
||||
/<([A-Z][\w\d]*)\b[^>]*?>([\s\S]*?)<\/\1>/g,
|
||||
''
|
||||
);
|
||||
|
||||
return removedPaired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple helper to escape text for inclusion inside HTML text nodes.
|
||||
*/
|
||||
function escapeHtml(str: string | null | undefined): string {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape for attribute values (double-quoted).
|
||||
*/
|
||||
function escapeHtmlAttr(str: string | null | undefined): string {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
||||
}
|
||||
Reference in New Issue
Block a user