Port website to Astro & Tailwind (#5009)

This commit is contained in:
Felix Boehm
2026-01-20 10:34:49 +00:00
committed by GitHub
parent 3e442c0f26
commit 8f4e77a1c9
69 changed files with 5985 additions and 15794 deletions

View File

@@ -36,5 +36,9 @@ jobs:
- name: Build Cheerio
run: npm run build
- name: Build website
run: npm run build
working-directory: website
- name: Run lint
run: npm run lint

7
.gitignore vendored
View File

@@ -7,5 +7,8 @@ npm-debug.log
/.tshy
/.tshy-build
/dist
/website/docs/api
/website/build
# Website build artifacts
website/.astro/
website/dist/
website/src/content/docs/api

View File

@@ -15,14 +15,14 @@ const gitignorePath = fileURLToPath(new URL('.gitignore', import.meta.url));
export default defineConfig(
includeIgnoreFile(gitignorePath), // Handle .gitignore patterns
// 0. Global linter options
// Global linter options
{
linterOptions: {
reportUnusedDisableDirectives: true, // Enable reporting of unused disable directives
},
},
// 1. Base configurations for all relevant files
// Base configurations for all relevant files
eslintJs.configs.recommended, // Basic ESLint recommended rules
{
@@ -77,13 +77,14 @@ export default defineConfig(
},
},
// 2. Global custom rules and language options
// Global custom rules and language options
{
languageOptions: {
globals: globals.node,
parserOptions: {
projectService: {
allowDefaultProject: ['*.js'],
defaultProject: 'tsconfig.json',
},
tsconfigRootDir: import.meta.dirname, // eslint-disable-line n/no-unsupported-features/node-builtins
},
@@ -117,7 +118,7 @@ export default defineConfig(
},
},
// 3. TypeScript specific configurations
// TypeScript specific configurations
tseslint.configs.recommendedTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
@@ -164,7 +165,7 @@ export default defineConfig(
},
},
// 4. Vitest specific configuration (for *.spec.ts files)
// Vitest specific configuration (for *.spec.ts files)
{
files: ['**/*.spec.ts'],
plugins: { vitest: eslintPluginVitest },
@@ -182,6 +183,19 @@ export default defineConfig(
},
},
// 5. Prettier - must be the last configuration to override styling rules
// Website specific configuration
{
files: ['website/**/*.ts', 'website/**/*.tsx', 'website/**/*.mts'],
languageOptions: {
parserOptions: {
projectService: {
allowDefaultProject: ['*.mjs'],
},
tsconfigRootDir: `${import.meta.dirname}/website`, // eslint-disable-line n/no-unsupported-features/node-builtins
},
},
},
// Prettier - must be the last configuration to override styling rules
eslintConfigPrettier,
);

35
website/astro.config.mjs Normal file
View File

@@ -0,0 +1,35 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import react from '@astrojs/react';
import sitemap from '@astrojs/sitemap';
import tailwindcss from '@tailwindcss/vite';
import remarkDirective from 'remark-directive';
import { remarkAdmonitions } from './src/plugins/remark-admonitions.ts';
import { remarkFixTypedocLinks } from './src/plugins/remark-fix-typedoc-links.ts';
import { remarkLiveCode } from './src/plugins/remark-live-code.ts';
import { rehypeExternalLinks } from './src/plugins/rehype-external-links.ts';
export default defineConfig({
site: 'https://cheerio.js.org',
integrations: [mdx(), react(), sitemap()],
image: {
remotePatterns: [
{
protocol: 'https',
hostname: 'unavatar.io',
},
],
},
vite: {
plugins: [tailwindcss()],
},
markdown: {
remarkPlugins: [
remarkDirective,
remarkAdmonitions,
remarkFixTypedocLinks,
remarkLiveCode,
],
rehypePlugins: [rehypeExternalLinks],
},
});

View File

@@ -1,3 +0,0 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

View File

@@ -1,7 +0,0 @@
fb55:
name: Felix Boehm
title: Maintainer of Cheerio
url: https://feedic.com/
image_url: https://github.com/fb55.png
socials:
github: fb55

View File

@@ -1,13 +0,0 @@
project_id: '567683'
api_token_env: CROWDIN_PERSONAL_TOKEN
preserve_hierarchy: true
files:
# JSON translation files
- source: /i18n/en/**/*
translation: /i18n/%two_letters_code%/**/%original_file_name%
# Docs Markdown files
- source: /docs/**/*
translation: /i18n/%two_letters_code%/docusaurus-plugin-content-docs/current/**/%original_file_name%
# Blog Markdown files
- source: /blog/**/*
translation: /i18n/%two_letters_code%/docusaurus-plugin-content-blog/**/%original_file_name%

View File

@@ -1,7 +0,0 @@
{
"label": "Tutorials - Advanced",
"position": 3,
"link": {
"type": "generated-index"
}
}

View File

@@ -1,7 +0,0 @@
{
"label": "Tutorials - Basics",
"position": 2,
"link": {
"type": "generated-index"
}
}

View File

@@ -1,262 +0,0 @@
// @ts-check
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { themes } = require('prism-react-renderer');
// eslint-disable-next-line @typescript-eslint/no-require-imports
const packageJson = require('../package.json');
/** @type {import('@docusaurus/types').Config} */
const config = {
title: packageJson.name,
tagline: packageJson.description,
url: packageJson.homepage,
baseUrl: '/',
trailingSlash: false,
onBrokenLinks: 'warn',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico',
// GitHub pages deployment config.
organizationName: 'cheeriojs', // Usually your GitHub org/user name.
projectName: 'cheerio', // Usually your repo name.
/*
* Even if you don't use internalization, you can use this field to set useful
* metadata like html lang. For example, if your site is Chinese, you may want
* to replace "en" with "zh-Hans".
*/
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
future: {
experimental_faster: true,
v4: true,
},
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
editUrl: 'https://github.com/cheeriojs/cheerio/tree/main/website/',
remarkPlugins: [
// eslint-disable-next-line @typescript-eslint/no-require-imports
[require('@docusaurus/remark-plugin-npm2yarn'), { sync: true }],
],
},
blog: {
showReadingTime: true,
editUrl: 'https://github.com/cheeriojs/cheerio/tree/main/website/',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
gtag: {
trackingID: 'G-PZHRH775FB',
},
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: {
title: 'Cheerio',
logo: {
alt: 'Cheerio Logo',
src: 'img/orange-c.svg',
},
items: [
{
type: 'doc',
docId: 'intro',
position: 'left',
label: 'Tutorial',
},
{
to: 'docs/api',
label: 'API',
position: 'left',
},
{ to: '/blog', label: 'Blog', position: 'left' },
{
href: 'https://github.com/cheeriojs/cheerio',
label: 'GitHub',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'Docs',
items: [
{
label: 'Tutorial',
to: '/docs/intro',
},
{
label: 'API',
to: 'docs/api',
},
],
},
{
title: 'Community',
items: [
{
label: 'Stack Overflow',
href: 'https://stackoverflow.com/questions/tagged/cheerio',
},
{
label: 'GitHub',
href: 'https://github.com/cheeriojs/cheerio',
},
/*
* {
* label: 'Twitter',
* href: 'https://twitter.com/docusaurus',
* },
*/
],
},
{
title: 'More',
items: [
{
label: 'Blog',
to: '/blog',
},
{
label: 'Attribution',
href: '/attribution',
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} The Cheerio contributors`,
},
metadata: [
{
name: 'keywords',
content: `${packageJson.keywords.join(', ')}, nodejs`,
},
],
prism: {
theme: themes.github,
darkTheme: themes.dracula,
},
algolia: {
appId: 'NRR2XU4QSP',
apiKey: '9d30ee79d65ccc63b95e693124e05405',
indexName: 'crawler_cheerio',
},
}),
themes: ['@docusaurus/theme-live-codeblock'],
plugins: [
[
'client-redirects',
/** @type {import('@docusaurus/plugin-client-redirects').Options} */
({
fromExtensions: ['html'],
redirects: [
// Classes
{
from: `/classes/Cheerio.html`,
to: `/docs/api/classes/Cheerio`,
},
// Interfaces
...['CheerioAPI', 'CheerioOptions', 'HTMLParser2Options'].map(
(name) => ({
from: `/interfaces/${name}.html`,
to: `/docs/api/interfaces/${name}`,
}),
),
// Type aliases and functions
{
/*
* Type aliases and functions are all part of the `api` page. We
* unfortunately can't redirect to a specific function, so we
* redirect to the top of the page.
*/
from: [
...[
'AcceptedElems',
'AcceptedFilters',
'AnyNode',
'BasicAcceptedElems',
'FilterFunction',
'ParentNode',
'SelectorType',
].map((name) => `/types/${name}.html`),
...[
'contains',
'default',
'html',
'load',
'merge',
'parseHTML',
'root',
'text',
'xml',
].map((name) => `/functions/${name}.html`),
],
to: '/docs/api',
},
],
}),
],
[
'docusaurus-plugin-typedoc',
{
// TypeDoc options
entryPoints: ['../src/index.ts'],
tsconfig: '../tsconfig.typedoc.json',
readme: 'none',
excludePrivate: true,
externalSymbolLinkMappings: {
domhandler: {
Document: 'https://domhandler.js.org/classes/Document.html',
Element: 'https://domhandler.js.org/classes/Element.html',
Node: 'https://domhandler.js.org/classes/Node.html',
AnyNode: 'https://domhandler.js.org/types/AnyNode.html',
ChildNode: 'https://domhandler.js.org/types/ChildNode.html',
ParentNode: 'https://domhandler.js.org/types/ParentNode.html',
DomHandlerOptions:
'https://domhandler.js.org/interfaces/DomHandlerOptions.html',
},
parse5: {
ParserOptions:
'https://parse5.js.org/interfaces/parse5.ParserOptions.html',
},
},
plugin: ['typedoc-plugin-mdn-links'],
// Plugin options
sidebar: {
// Always display the API entry last
position: Number.MAX_SAFE_INTEGER,
pretty: true,
},
outputFileStrategy: 'members',
},
],
],
};
module.exports = config;

19113
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +1,43 @@
{
"name": "@cheerio/website",
"type": "module",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"crowdin:sync": "docusaurus write-translations && crowdin upload && crowdin download",
"lint:ts": "tsc --noEmit"
"dev": "astro dev",
"start": "astro dev",
"build": "npm run build:api && astro build",
"build:api": "typedoc",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@docusaurus/core": "^3.9.2",
"@docusaurus/module-type-aliases": "^3.9.2",
"@docusaurus/plugin-client-redirects": "^3.9.2",
"@docusaurus/preset-classic": "^3.9.2",
"@docusaurus/remark-plugin-npm2yarn": "^3.9.2",
"@docusaurus/theme-live-codeblock": "^3.9.2",
"@mdx-js/react": "^3.1.1",
"clsx": "^2.1.1",
"docusaurus-plugin-typedoc": "^1.4.2",
"prism-react-renderer": "^2.4.1",
"react": "^19.2.3",
"@astrojs/mdx": "^4.3.0",
"@astrojs/react": "^4.3.0",
"@astrojs/rss": "^4.0.15",
"@astrojs/sitemap": "^3.7.0",
"@codesandbox/sandpack-react": "^2.19.0",
"@docsearch/css": "^4.4.0",
"@docsearch/js": "^4.4.0",
"@tailwindcss/vite": "^4.1.8",
"astro": "^5.9.0",
"hastscript": "^9.0.1",
"marked": "^17.0.1",
"react": "^19.1.0",
"react-dom": "^19.2.3",
"typedoc": "^0.28.16",
"typedoc-plugin-markdown": "^4.9.0",
"typedoc-plugin-mdn-links": "^5.1.0"
"remark-directive": "^4.0.0",
"tailwindcss": "^4.1.8",
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
"@crowdin/cli": "^4.12.0",
"@docusaurus/faster": "^3.9.2",
"@tsconfig/docusaurus": "^2.0.7",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"typedoc": "^0.28.5",
"typedoc-plugin-markdown": "^4.6.3",
"typedoc-plugin-mdn-links": "^5.0.1",
"typescript": "^5.9.3"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"engines": {
"node": ">=16.14"
"node": ">=20.18.1"
}
}

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 724 B

After

Width:  |  Height:  |  Size: 724 B

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 955 B

After

Width:  |  Height:  |  Size: 955 B

View File

@@ -0,0 +1,47 @@
---
const features = [
{
title: 'Proven syntax',
icon: '/img/1F496.svg',
description:
"Cheerio implements a subset of core jQuery. Cheerio removes all the DOM inconsistencies and browser cruft from the jQuery library, revealing its truly gorgeous API.",
},
{
title: 'Blazingly fast',
icon: '/img/26A1.svg',
description:
'Cheerio works with a very simple, consistent DOM model. As a result parsing, manipulating, and rendering are incredibly efficient.',
},
{
title: 'Incredibly flexible',
icon: '/img/1F57A.svg',
description:
'Cheerio can parse nearly any HTML or XML document. Cheerio works in both browser and server environments.',
},
];
---
<section class="bg-white py-16 dark:bg-slate-900">
<div class="mx-auto max-w-7xl px-4">
<div class="grid grid-cols-1 gap-12 md:grid-cols-3">
{
features.map((feature) => (
<div class="text-center">
<div class="mb-4 flex justify-center">
<img
src={feature.icon}
alt=""
class="h-32 w-32"
aria-hidden="true"
/>
</div>
<h3 class="mb-2 text-xl font-semibold">{feature.title}</h3>
<p class="text-slate-600 dark:text-slate-400">
{feature.description}
</p>
</div>
))
}
</div>
</div>
</section>

View File

@@ -0,0 +1,56 @@
---
const footerLinks = {
Docs: [
{ label: 'Tutorial', href: '/docs/intro' },
{ label: 'API', href: '/docs/api' },
],
Community: [
{
label: 'Stack Overflow',
href: 'https://stackoverflow.com/questions/tagged/cheerio',
},
{ label: 'GitHub', href: 'https://github.com/cheeriojs/cheerio' },
],
More: [
{ label: 'Blog', href: '/blog' },
{ label: 'Attribution', href: '/attribution' },
],
};
const currentYear = new Date().getFullYear();
---
<footer class="bg-slate-900 py-12 text-slate-300">
<div class="mx-auto max-w-7xl px-4">
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
{
Object.entries(footerLinks).map(([title, links]) => (
<div>
<h3 class="mb-4 font-semibold text-white">{title}</h3>
<ul class="space-y-2">
{links.map((link) => (
<li>
<a
href={link.href}
class="transition-colors hover:text-primary-light"
target={link.href.startsWith('http') ? '_blank' : undefined}
rel={
link.href.startsWith('http')
? 'noopener noreferrer'
: undefined
}
>
{link.label}
</a>
</li>
))}
</ul>
</div>
))
}
</div>
<div class="mt-8 border-t border-slate-700 pt-8 text-center text-sm">
Copyright &copy; {currentYear} The Cheerio contributors
</div>
</div>
</footer>

View File

@@ -0,0 +1,18 @@
---
const title = 'cheerio';
const tagline =
'The fast, flexible & elegant library for parsing and manipulating HTML and XML.';
---
<header class="bg-primary py-16 text-center text-white">
<div class="mx-auto max-w-4xl px-4">
<h1 class="mb-4 text-5xl font-bold">{title}</h1>
<p class="mb-8 text-xl opacity-90">{tagline}</p>
<a
href="/docs/intro"
class="inline-block rounded-lg bg-white px-8 py-3 font-semibold text-primary transition-colors hover:bg-slate-100"
>
Get Started!
</a>
</div>
</header>

View File

@@ -0,0 +1,11 @@
---
/**
* Astro component that wraps LiveCode for use in markdown.
* Usage in markdown: <LiveEditor>...</LiveEditor>
*/
import { LiveCode } from './live-code';
---
<div class="live-editor-wrapper">
<LiveCode client:load code={await Astro.slots.render('default')} />
</div>

View File

@@ -0,0 +1,99 @@
---
const navItems = [
{ label: 'Tutorial', href: '/docs/intro' },
{ label: 'API', href: '/docs/api' },
{ label: 'Blog', href: '/blog' },
];
---
<nav class="sticky top-0 z-50 border-b border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3">
<div class="flex items-center gap-8">
<a href="/" class="flex items-center gap-2">
<img src="/img/orange-c.svg" alt="Cheerio Logo" class="h-8 w-8" />
<span class="text-xl font-semibold">Cheerio</span>
</a>
<div class="hidden items-center gap-6 md:flex">
{
navItems.map((item) => (
<a
href={item.href}
class="text-sm font-medium text-slate-600 transition-colors hover:text-primary dark:text-slate-300 dark:hover:text-primary-light"
>
{item.label}
</a>
))
}
</div>
</div>
<div class="flex items-center gap-4">
<!-- DocSearch container -->
<div id="docsearch" class="hidden md:block"></div>
<a
href="https://github.com/cheeriojs/cheerio"
target="_blank"
rel="noopener noreferrer"
class="text-slate-600 transition-colors hover:text-primary dark:text-slate-300"
aria-label="GitHub"
>
<svg viewBox="0 0 24 24" class="h-6 w-6" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
<!-- Mobile menu button -->
<button
type="button"
class="inline-flex items-center justify-center rounded-md p-2 text-slate-600 hover:bg-slate-100 hover:text-slate-900 md:hidden dark:text-slate-300 dark:hover:bg-slate-800"
aria-label="Open menu"
id="mobile-menu-button"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
<!-- Mobile menu -->
<div id="mobile-menu" class="hidden md:hidden">
<div class="space-y-1 px-4 pb-3 pt-2">
{
navItems.map((item) => (
<a
href={item.href}
class="block rounded-md px-3 py-2 text-base font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
>
{item.label}
</a>
))
}
</div>
</div>
</nav>
<script>
// Mobile menu toggle
const menuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
menuButton?.addEventListener('click', () => {
mobileMenu?.classList.toggle('hidden');
});
</script>
<script>
// Initialize DocSearch
import docsearch from '@docsearch/js';
import '@docsearch/css';
docsearch({
appId: 'NRR2XU4QSP',
apiKey: '9d30ee79d65ccc63b95e693124e05405',
indexName: 'crawler_cheerio',
container: '#docsearch',
});
</script>

View File

@@ -0,0 +1,78 @@
---
interface SidebarItem {
label: string;
href: string;
}
interface SidebarSection {
title: string;
items: SidebarItem[];
}
interface Props {
currentPath: string;
}
const { currentPath } = Astro.props;
const sidebar: SidebarSection[] = [
{
title: 'Getting Started',
items: [{ label: 'Introduction', href: '/docs/intro' }],
},
{
title: 'Basics',
items: [
{ label: 'Loading Documents', href: '/docs/basics/loading' },
{ label: 'Selecting Elements', href: '/docs/basics/selecting' },
{ label: 'Traversing the DOM', href: '/docs/basics/traversing' },
{ label: 'Manipulating Elements', href: '/docs/basics/manipulation' },
],
},
{
title: 'Advanced',
items: [
{ label: 'Configuring Cheerio', href: '/docs/advanced/configuring-cheerio' },
{ label: 'Extending Cheerio', href: '/docs/advanced/extending-cheerio' },
{ label: 'Extracting Data', href: '/docs/advanced/extract' },
],
},
{
title: 'Reference',
items: [{ label: 'API Documentation', href: '/docs/api' }],
},
];
---
<aside
class="hidden w-64 shrink-0 border-r border-slate-200 dark:border-slate-700 lg:block"
>
<nav class="sticky top-0 h-screen overflow-y-auto px-6 py-8">
{
sidebar.map((section, index) => (
<div class={index === 0 ? 'mb-8' : 'mb-8'}>
<h3 class="mb-3 text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500">
{section.title}
</h3>
<ul class="space-y-1">
{section.items.map((item) => (
<li>
<a
href={item.href}
class:list={[
'block rounded-lg px-3 py-2 text-sm transition-all duration-150',
currentPath === item.href
? 'bg-primary/10 font-semibold text-primary border-l-2 border-primary'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-slate-100',
]}
>
{item.label}
</a>
</li>
))}
</ul>
</div>
))
}
</nav>
</aside>

View File

@@ -0,0 +1,47 @@
---
import sponsorsData from '../../sponsors.json';
interface Sponsor {
name: string;
image: string;
url: string;
}
const headliners = sponsorsData.headliner as Sponsor[];
---
<section class="bg-slate-50 py-12 dark:bg-slate-800">
<div class="mx-auto max-w-7xl px-4">
<h2 class="mb-8 text-center text-2xl font-semibold">
Supported and Backed by
</h2>
<div class="flex flex-wrap items-center justify-center gap-8">
{
headliners.map((sponsor) => (
<a
href={sponsor.url}
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-3 rounded-lg p-3 transition-colors hover:bg-slate-100 dark:hover:bg-slate-700"
>
<img
src={sponsor.image}
alt={`${sponsor.name} logo`}
class="h-12 w-12 rounded-lg"
/>
<span class="font-medium">{sponsor.name}</span>
</a>
))
}
<a
href="https://github.com/sponsors/cheeriojs"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-3 rounded-lg border border-slate-300 p-3 transition-all hover:border-primary hover:shadow-md dark:border-slate-600 dark:hover:border-primary-light"
>
<img src="/img/1F496.svg" alt="" class="h-12 w-12" aria-hidden="true" />
<span class="font-medium">...and you?</span>
</a>
</div>
</div>
</section>

View File

@@ -0,0 +1,39 @@
---
interface Props {
headings: Array<{
depth: number;
slug: string;
text: string;
}>;
}
const { headings } = Astro.props;
// Filter to only h2 and h3 headings
const toc = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
---
{toc.length > 0 && (
<nav class="hidden xl:block w-56 shrink-0 pl-6 pt-4">
<div class="sticky top-8">
<h4 class="mb-3 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">
On this page
</h4>
<ul class="space-y-2 text-xs">
{toc.map((heading) => (
<li>
<a
href={`#${heading.slug}`}
class:list={[
'block text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200 transition-colors leading-snug',
heading.depth === 2 ? 'font-medium text-slate-600 dark:text-slate-300' : 'pl-3 text-slate-500',
]}
>
{heading.text}
</a>
</li>
))}
</ul>
</div>
</nav>
)}

View File

@@ -0,0 +1,132 @@
---
import { Image } from 'astro:assets';
interface Tweet {
id: string;
name: string;
user: string;
date: string;
tweet: string;
}
const tweets: Tweet[] = [
{
id: '628016191928446977',
name: 'Axel Rauschmayer',
user: 'rauschma',
date: '2015-08-03T02:35:00.000Z',
tweet:
"For transforming HTML via Node.js scripts, @mattmueller's cheerio works really well.",
},
{
id: '1616150822932385792',
name: 'Valeri Karpov',
user: 'code_barbarian',
date: '2023-01-19T19:09:00.000Z',
tweet:
"Cheerio is a weird npm module: most devs have never heard of it, but I rarely build an app without it. So much utility for quick and easy HTML transformations.",
},
{
id: '1545481085865320449',
name: 'Thomas Boutell',
user: 'boutell',
date: '2021-07-08T19:52:00.000Z',
tweet:
"You probably shouldn't use jQuery, but if you're great at jQuery, you're going to be really popular on server-side projects that need web scraping or HTML transformation. \"npm install cheerio\" ahoy!",
},
{
id: '552311181760008192',
name: 'Alistair G MacDonald',
user: 'html5js',
date: '2015-01-06T03:50:00.000Z',
tweet:
'Looking for a faster, cleaner alternative to basic JSDOM? Try Cheerio! #npm #javascript #nodejs',
},
{
id: '1466753169900273667',
name: 'Yogini Bende',
user: 'hey_yogini',
date: '2021-12-03T12:56:00.000Z',
tweet: 'Cheerio is fire',
},
{
id: '936243649234591744',
name: 'Jonny Frodsham',
user: 'jonnyfrodsham',
date: '2017-11-30T14:40:00.000Z',
tweet:
"Needed to do a quick web scrape in Node for a demo. Seems like I'm back using jQuery in the super timesaving cheerio npm package",
},
{
id: '264033999272439809',
name: 'Thomas Steiner',
user: 'tomayac',
date: '2012-11-01T15:59:00.000Z',
tweet:
"npm install cheerio. That's the #jQuery DOM API for #nodeJS essentially. Thanks, @MattMueller",
},
{
id: '1403139379757977602',
name: 'Mike Pennisi',
user: 'JugglinMike',
date: '2021-06-11T04:57:00.000Z',
tweet:
"Thank you @fb55 for tirelessly pushing Cheerio to version 1.0. That library helps so many developers expand their horizons beyond the browser, and you've been making it possible for a decade!",
},
{
id: '1186972238190403587',
name: 'Matthew Phillips',
user: 'matthewcp',
date: '2019-10-23T12:46:00.000Z',
tweet:
'Cheerio is (still) such a useful tool for manipulating HTML. Shout to @MattMueller for saving me an untold amount of time over the years.',
},
];
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
---
<section class="py-16">
<div class="mx-auto max-w-7xl px-4">
<h2 class="mb-8 text-center text-2xl font-semibold">What Our Users Say</h2>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{
tweets.map((tweet) => (
<div class="rounded-lg border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-800">
<div class="mb-4 flex items-center gap-3">
<Image
src={`https://unavatar.io/${tweet.user}`}
alt={`${tweet.name}'s avatar`}
width={40}
height={40}
class="h-10 w-10 rounded-full"
loading="lazy"
/>
<div>
<div class="font-medium">{tweet.name}</div>
<div class="text-sm text-slate-500 dark:text-slate-400">
@{tweet.user}
</div>
</div>
</div>
<p class="mb-4 text-slate-700 dark:text-slate-300">{tweet.tweet}</p>
<a
href={`https://twitter.com/${tweet.user}/status/${tweet.id}`}
target="_blank"
rel="noopener noreferrer"
class="text-sm text-slate-500 transition-colors hover:text-primary dark:text-slate-400"
>
{formatDate(tweet.date)}
</a>
</div>
))
}
</div>
</div>
</section>

View File

@@ -1,70 +0,0 @@
import React from 'react';
import Heart from '@site/static/img/1F496.svg';
import Lightning from '@site/static/img/26A1.svg';
import PersonRunning from '@site/static/img/1F57A.svg';
interface FeatureItem {
title: string;
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
description: JSX.Element;
}
const FeatureList: FeatureItem[] = [
{
title: 'Proven syntax',
Svg: Heart,
description: (
<>
Cheerio implements a subset of core jQuery. Cheerio removes all the DOM
inconsistencies and browser cruft from the jQuery library, revealing its
truly gorgeous API.
</>
),
},
{
title: 'Blazingly fast',
Svg: Lightning,
description: (
<>
Cheerio works with a very simple, consistent DOM model. As a result
parsing, manipulating, and rendering are incredibly efficient.
</>
),
},
{
title: 'Incredibly flexible',
Svg: PersonRunning,
description: (
<>
Cheerio can parse nearly any HTML or XML document. Cheerio works in both
browser and server environments.
</>
),
},
];
function Feature({ title, Svg, description }: FeatureItem) {
return (
<div className="col col--4">
<div className="text--center">
<Svg height="200" width="200" role="img" />
</div>
<div className="text--center padding-horiz--md">
<h3>{title}</h3>
<p>{description}</p>
</div>
</div>
);
}
export function HomepageFeatures(): JSX.Element {
return (
<section className="container padding-top--xl ">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</section>
);
}

View File

@@ -1,21 +0,0 @@
.emphasis {
background-color: var(--ifm-color-emphasis-100);
}
.you {
border: 1px solid var(--ifm-color-emphasis-400);
border-radius: 10px;
color: var(--ifm-font-color-base);
transition:
border 0.2s ease-in-out,
background-color 0.2s ease-in-out,
color 0.2s ease-in-out,
box-shadow 0.2s ease-in-out;
}
.you:hover {
border: 1px solid var(--ifm-color-emphasis-200);
background-color: var(--ifm-color-info-lightest);
color: var(--ifm-font-color-base);
box-shadow: 0 0 0.5rem 0 rgba(150, 0, 0, 0.2);
}

View File

@@ -1,50 +0,0 @@
import React from 'react';
import HeartSvg from '@site/static/img/1F496.svg';
import styles from './homepage-sponsors.module.css';
import Sponsors from '../../sponsors.json';
export function HeadlineSponsors() {
return (
<div className={`padding-vert--lg margin-vert--lg ${styles.emphasis}`}>
<div className="container">
<h2 className="text--center">Supported and Backed by</h2>
<div
className="container row"
style={{ justifyContent: 'space-evenly' }}
>
{Sponsors.headliner.map((sponsor) => (
<div className="col col--2 avatar row row--align-center margin-top--sm">
<a
className="avatar__photo-link avatar__photo avatar__photo--lg"
style={{ borderRadius: '10px' }}
href={sponsor.url}
key={sponsor.name}
target="_blank"
rel="noopener noreferrer"
>
<img src={sponsor.image} alt={`${sponsor.name} logo`} />
</a>
<div className="avatar__intro">
<div className="avatar__name">{sponsor.name}</div>
</div>
</div>
))}
<a
className={`col col--2 avatar row padding--md margin-top--sm ${styles.you}`}
href="https://github.com/sponsors/cheeriojs"
target="_blank"
rel="noopener noreferrer"
>
<HeartSvg className="avatar__photo-link avatar__photo avatar__photo--lg" />
<div className="avatar__intro">
<div className="avatar__name">and you?</div>
</div>
</a>
</div>
</div>
</div>
);
}

View File

@@ -1,175 +0,0 @@
import React from 'react';
interface TweetItem {
id: string;
name: string;
user: string;
date: string;
tweet: React.ReactNode;
}
const TweetList = [
{
id: '628016191928446977',
name: 'Axel Rauschmayer',
user: 'rauschma',
date: '2015-08-03T02:35:00.000Z',
tweet: (
<>
For transforming HTML via Node.js scripts, @mattmueller's cheerio works
really well.
</>
),
},
{
id: '1616150822932385792',
name: 'Valeri Karpov',
user: 'code_barbarian',
date: '2023-01-19T19:09:00.000Z',
tweet: (
<>
Cheerio is a weird npm module: most devs have never heard of it, but I
rarely build an app without it.
<br />
<br />
So much utility for quick and easy HTML transformations.
</>
),
},
{
id: '1545481085865320449',
name: 'Thomas Boutell',
user: 'boutell',
date: '2021-07-08T19:52:00.000Z',
tweet: (
<>
You probably shouldn't use jQuery, but if you're great at jQuery, you're
going to be really popular on server-side projects that need web
scraping or HTML transformation. "npm install cheerio" ahoy!
</>
),
},
{
id: '552311181760008192',
name: 'Alistair G MacDonald',
user: 'html5js',
date: '2015-01-06T03:50:00.000Z',
tweet: (
<>
Looking for a faster, cleaner alternative to basic JSDOM? Try Cheerio!
#npm #javascript #nodejs
</>
),
},
{
id: '1466753169900273667',
name: 'Yogini Bende',
user: 'hey_yogini',
date: '2021-12-03T12:56:00.000Z',
tweet: 'Cheerio is 🔥',
},
{
id: '936243649234591744',
name: 'Jonny Frodsham',
user: 'jonnyfrodsham',
date: '2017-11-30T14:40:00.000Z',
tweet: (
<>
Needed to do a quick web scrape in Node for a demo. Seems like I'm back
using jQuery in the super timesaving cheerio npm package 😯
</>
),
},
{
id: '264033999272439809',
name: 'Thomas Steiner',
user: 'tomayac',
date: '2012-11-01T15:59:00.000Z',
tweet: (
<>
npm install cheerio. That's the #jQuery DOM API for #nodeJS essentially.
Thanks, @MattMueller
</>
),
},
{
id: '1403139379757977602',
name: 'Mike Pennisi',
user: 'JugglinMike',
date: '2021-06-11T04:57:00.000Z',
tweet: (
<>
Thank you @fb55 for tirelessly pushing Cheerio to version 1.0. That
library helps so many developers expand their horizons beyond the
browser, and you've been making it possible for a decade!
</>
),
},
{
id: '1186972238190403587',
name: 'Matthew Phillips',
user: 'matthewcp',
date: '2019-10-23T12:46:00.000Z',
tweet: (
<>
Cheerio is (still) such a useful tool for manipulating HTML. Shout to
@MattMueller for saving me an untold amount of time over the years.
</>
),
},
];
function Tweet({ id, name, user, date, tweet }: TweetItem) {
const formattedDate = new Date(date).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true,
});
return (
<div className="card-demo col col--4 margin-vert--sm">
<div className="card">
<div className="card__header">
<div className="avatar">
<img
className="avatar__photo"
src={`https://unavatar.io/${user}`}
alt={`${name}'s avatar`}
loading="lazy"
/>
<div className="avatar__intro">
<div className="avatar__name">{name}</div>
<small className="avatar__subtitle">@{user}</small>
</div>
</div>
</div>
<div className="card__body">{tweet}</div>
<time className="card__footer">
<a
href={`https://twitter.com/${user}/status/${id}`}
target="_blank"
rel="noopener noreferrer"
>
{formattedDate}
</a>
</time>
</div>
</div>
);
}
export function HomepageTweets() {
return (
<div className="container">
<h2 className="text--center">What Our Users Say</h2>
<div className="row">
{TweetList.map((props) => (
<Tweet {...props} />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,94 @@
import { useCallback } from 'react';
import {
SandpackProvider,
SandpackCodeEditor,
SandpackConsole,
useSandpack,
} from '@codesandbox/sandpack-react';
interface LiveCodeProps {
code: string;
}
function ResetButton() {
const { sandpack } = useSandpack();
const handleReset = useCallback(() => sandpack.resetAllFiles(), [sandpack]);
return (
<button
onClick={handleReset}
className="px-2 py-1 text-xs font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-200 dark:hover:bg-slate-700 rounded transition-colors"
title="Reset code and re-run"
>
Reset
</button>
);
}
function RunButton() {
const { sandpack } = useSandpack();
const handleRun = () => {
const { code } = sandpack.files['/index.js'];
sandpack.updateFile('/index.js', code, true);
};
return (
<button
onClick={handleRun}
className="px-2 py-1 text-xs font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-200 dark:hover:bg-slate-700 rounded transition-colors"
title="Run code"
>
Run
</button>
);
}
function Toolbar() {
return (
<div className="flex items-center justify-between px-3 py-2 bg-slate-100 dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700">
<span className="text-xs font-medium text-slate-600 dark:text-slate-400 uppercase tracking-wide">
Live Editor
</span>
<div className="flex items-center gap-2">
<RunButton />
<ResetButton />
</div>
</div>
);
}
export function LiveCode({ code }: LiveCodeProps) {
// Wrap user code to run immediately and output via console.log
const wrappedCode = `import * as cheerio from 'cheerio';
${code}
`;
return (
<div className="my-4 overflow-hidden rounded-lg border border-slate-200 dark:border-slate-700 not-prose">
<SandpackProvider
template="vanilla"
theme="auto"
files={{
'/index.js': wrappedCode,
}}
customSetup={{
dependencies: {
cheerio: 'latest',
},
}}
>
<Toolbar />
<SandpackCodeEditor showLineNumbers style={{ height: '200px' }} />
<SandpackConsole
style={{ height: '150px' }}
standalone
showHeader
showResetConsoleButton
/>
</SandpackProvider>
</div>
);
}

View File

@@ -5,8 +5,6 @@ authors: fb55
tags: [release, announcement]
---
# Cheerio 1.0 Released, Batteries Included 🔋
Cheerio 1.0 is out! After 12 release candidates and just a short seven years
after the initial 1.0 release candidate, it is finally time to call Cheerio 1.0
complete. The theme for this release is "batteries included", with common use

View File

@@ -0,0 +1,24 @@
import { defineCollection, z } from 'astro:content';
const docs = defineCollection({
type: 'content',
schema: z.object({
title: z.string().optional(),
description: z.string().optional(),
sidebar_position: z.number().optional(),
sidebar_label: z.string().optional(),
}),
});
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
slug: z.string().optional(),
authors: z.union([z.string(), z.array(z.string())]).optional(),
tags: z.array(z.string()).optional(),
date: z.date().optional(),
}),
});
export const collections = { docs, blog };

View File

@@ -27,7 +27,7 @@ For example, if you want the contents of `<noscript>` tags to be parsed as HTML,
you can set the `scriptingEnabled` option to false.
For a full list of options and their effects, have a look at
[the API documentation](/docs/api/interfaces/Parse5Options).
[the API documentation](/docs/api/interfaces/CheerioOptions).
### Fragment Mode

View File

@@ -16,7 +16,7 @@ document.
:::
:::danger Availability of methods
:::danger[Availability of methods]
The `loadBuffer`, `stringStream`, `decodeStream`, and `fromURL` methods are not
available in the browser environment. Instead, use the `load` method to parse
@@ -57,7 +57,7 @@ $.html();
:::
Learn more about the `load` method in the
[API documentation](/docs/api/functions/load).
[API documentation](/docs/api/variables/load).
## `loadBuffer`

View File

@@ -104,7 +104,7 @@ $('h1').text('Hello, World!');
const text = $('p').text();
```
:::tip Note
:::tip[Note]
`text()` returns the `textContent` of all passed elements. The result will
include the contents of `<script>` and `<style>` elements. To avoid this, use

View File

@@ -54,7 +54,7 @@ const $selected = $('.selected');
const $selected = $('[data-selected=true]');
```
:::tip XML Namespaces
:::tip[XML Namespaces]
You can select with XML Namespaces but
[due to the CSS specification](https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#attribute-selectors),

View File

@@ -3,6 +3,8 @@ sidebar_position: 4
description: Traverse the DOM tree and filter elements.
---
import { LiveCode } from '@/components/live-code';
# Traversing the DOM
Traversing a document with Cheerio allows you to select and manipulate specific
@@ -40,7 +42,7 @@ the current selection.
Here's an example of using `find` to select all `<li>` elements within a `<ul>`
element:
```js live noInline
```js live
const $ = cheerio.load(
`<ul>
<li>Item 1</li>
@@ -49,7 +51,7 @@ const $ = cheerio.load(
);
const listItems = $('ul').find('li');
render(<>List item count: {listItems.length}</>);
console.log(`List item count: ${listItems.length}`);
```
### `children`
@@ -61,7 +63,7 @@ direct children of the current selection.
Here's an example of using `children` to select all `<li>` elements within a
`<ul>` element:
```js live noInline
```js live
const $ = cheerio.load(
`<ul>
<li>Item 1</li>
@@ -70,7 +72,7 @@ const $ = cheerio.load(
);
const listItems = $('ul').children('li');
render(<>List item count: {listItems.length}</>);
console.log(`List item count: ${listItems.length}`);
```
### `contents`
@@ -82,7 +84,7 @@ selection containing all children of the current selection.
Here's an example of using `contents` to select all children of a `<div>`
element:
```js live noInline
```js live
const $ = cheerio.load(
`<div>
Text <p>Paragraph</p>
@@ -90,7 +92,7 @@ const $ = cheerio.load(
);
const contents = $('div').contents();
render(<>Contents count: {contents.length}</>);
console.log(`Contents count: ${contents.length}`);
```
## Moving Up the DOM Tree
@@ -107,7 +109,7 @@ element of each element in the current selection.
Here's an example of using `parent` to select the parent `<ul>` element of a
`<li>` element:
```js live noInline
```js live
const $ = cheerio.load(
`<ul>
<li>Item 1</li>
@@ -115,7 +117,7 @@ const $ = cheerio.load(
);
const list = $('li').parent();
render(<>{list.prop('tagName')}</>);
console.log(list.prop('tagName'));
```
### `parents` and `parentsUntil`
@@ -132,7 +134,7 @@ selection up to (but not including) the specified ancestor.
Here's an example of using `parents` and `parentsUntil` to select ancestor
elements of a `<li>` element:
```js live noInline
```js live
const $ = cheerio.load(
`<div>
<ul>
@@ -144,14 +146,8 @@ const $ = cheerio.load(
const ancestors = $('li').parents();
const ancestorsUntil = $('li').parentsUntil('div');
render(
<>
<p>
Ancestor count (also includes "body" and "html" tags): {ancestors.length}
</p>
<p>Ancestor count (until "div"): {ancestorsUntil.length}</p>
</>,
);
console.log(`Ancestor count (includes body and html): ${ancestors.length}`);
console.log(`Ancestor count (until div): ${ancestorsUntil.length}`);
```
### `closest`
@@ -164,7 +160,7 @@ matching ancestor is found, the method returns an empty selection.
Here's an example of using `closest` to select the closest ancestor `<ul>`
element of a `<li>` element:
```js live noInline
```js live
const $ = cheerio.load(
`<div>
<ul>
@@ -174,7 +170,7 @@ const $ = cheerio.load(
);
const list = $('li').closest('ul');
render(<>{list.prop('tagName')}</>);
console.log(list.prop('tagName'));
```
## Moving Sideways Within the DOM Tree
@@ -196,7 +192,7 @@ containing the previous sibling element for each element in the given selection.
Here's an example of using `next` and `prev` to select sibling elements of a
`<li>` element:
```js live noInline
```js live
const $ = cheerio.load(
`<ul>
<li>Item 1</li>
@@ -207,12 +203,8 @@ const $ = cheerio.load(
const nextItem = $('li:first').next();
const prevItem = $('li:eq(1)').prev();
render(
<>
<p>Next: {nextItem.text()}</p>
<p>Prev: {prevItem.text()}</p>
</>,
);
console.log(`Next: ${nextItem.text()}`);
console.log(`Prev: ${prevItem.text()}`);
```
## `nextAll`, `prevAll`, and `siblings`
@@ -233,7 +225,7 @@ elements of each element in the current selection.
Here's an example of using `nextAll`, `prevAll`, and `siblings` to select
sibling elements of a `<li>` element:
```js live noInline
```js live
const $ = cheerio.load(
`<ul>
<li>[1]</li>
@@ -246,13 +238,9 @@ const nextAll = $('li:first').nextAll();
const prevAll = $('li:last').prevAll();
const siblings = $('li:eq(1)').siblings();
render(
<>
<p>Next All: {nextAll.text()}</p>
<p>Prev All: {prevAll.text()}</p>
<p>Siblings: {siblings.text()}</p>
</>,
);
console.log(`Next All: ${nextAll.text()}`);
console.log(`Prev All: ${prevAll.text()}`);
console.log(`Siblings: ${siblings.text()}`);
```
### `nextUntil` and `prevUntil`
@@ -272,7 +260,7 @@ element up to (but not including) the specified element.
Here's an example of using `nextUntil` and `prevUntil` to select sibling
elements of a `<li>` element:
```js live noInline
```js live
const $ = cheerio.load(
`<ul>
<li>Item 1</li>
@@ -284,12 +272,8 @@ const $ = cheerio.load(
const nextUntil = $('li:first').nextUntil('li:last-child');
const prevUntil = $('li:last').prevUntil('li:first-child');
render(
<>
<p>Next: {nextUntil.text()}</p>
<p>Prev: {prevUntil.text()}</p>
</>,
);
console.log(`Next: ${nextUntil.text()}`);
console.log(`Prev: ${prevUntil.text()}`);
```
## Filtering elements
@@ -313,7 +297,7 @@ returns a new selection containing the element at the specified index.
Here's an example of using `eq` to select the second `<li>` element within a
`<ul>` element:
```js live noInline
```js live
const $ = cheerio.load(
`<ul>
<li>Item 1</li>
@@ -322,7 +306,7 @@ const $ = cheerio.load(
);
const secondItem = $('li').eq(1);
render(<>{secondItem.text()}</>);
console.log(secondItem.text());
```
### `filter` and `not`
@@ -339,7 +323,7 @@ elements that do not match the selector.
Here's an example of using `filter` and `not` to select `<li>` elements within a
`<ul>` element:
```js live noInline
```js live
const $ = cheerio.load(
`<ul>
<li class="item">Item 1</li>
@@ -350,12 +334,8 @@ const $ = cheerio.load(
const matchingItems = $('li').filter('.item');
const nonMatchingItems = $('li').not('.item');
render(
<>
<p>Matching: {matchingItems.text()}</p>
<p>Non-matching: {nonMatchingItems.text()}</p>
</>,
);
console.log(`Matching: ${matchingItems.text()}`);
console.log(`Non-matching: ${nonMatchingItems.text()}`);
```
### `has`
@@ -368,7 +348,7 @@ an element matching the selector.
Here's an example of using `has` to select `<li>` elements within a `<ul>`
element that contain a `<strong>` element:
```js live noInline
```js live
const $ = cheerio.load(
`<ul>
<li>Item 1</li>
@@ -379,7 +359,7 @@ const $ = cheerio.load(
);
const matchingItems = $('li').has('strong');
render(<>{matchingItems.length}</>);
console.log(matchingItems.length);
```
### `first` and `last`
@@ -395,7 +375,7 @@ containing the last element.
Here's an example of using `first` and `last` to select elements within a `<ul>`
element:
```js live noInline
```js live
const $ = cheerio.load(
`<ul>
<li>Item 1</li>
@@ -406,12 +386,8 @@ const $ = cheerio.load(
const firstItem = $('li').first();
const lastItem = $('li').last();
render(
<>
<p>First: {firstItem.text()}</p>
<p>Last: {lastItem.text()}</p>
</>,
);
console.log(`First: ${firstItem.text()}`);
console.log(`Last: ${lastItem.text()}`);
```
## Conclusion

View File

@@ -1,55 +0,0 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url(../../static/fonts/inter.woff) format('woff');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Rubik';
font-style: normal;
font-weight: 400;
src: url(../../static/fonts/rubik.woff) format('woff');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #e88c1f;
--ifm-color-primary-dark: #d77e16;
--ifm-color-primary-darker: #cb7715;
--ifm-color-primary-darkest: #a76211;
--ifm-color-primary-light: #ea9837;
--ifm-color-primary-lighter: #ec9e43;
--ifm-color-primary-lightest: #efb167;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
--ifm-font-family-base: 'Rubik', system-ui, -apple-system, 'Segoe UI', Roboto,
Ubuntu, Cantarell, 'Noto Sans', sans-serif;
--ifm-heading-font-family: 'Inter', var(--ifm-font-family-base);
--ifm-heading-font-weight: 600;
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary: #f4a02f;
--ifm-color-primary-dark: #f39313;
--ifm-color-primary-darker: #eb8c0c;
--ifm-color-primary-darkest: #c1730a;
--ifm-color-primary-light: #f5ad4b;
--ifm-color-primary-lighter: #f6b358;
--ifm-color-primary-lightest: #f8c682;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}

5
website/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/* eslint-disable @typescript-eslint/triple-slash-reference */
/* eslint-disable spaced-comment */
/* eslint-disable multiline-comment-style */
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

View File

@@ -0,0 +1,41 @@
---
import '@/styles/global.css';
import { ViewTransitions } from 'astro:transitions';
import Navbar from '@/components/Navbar.astro';
import Footer from '@/components/Footer.astro';
interface Props {
title: string;
description?: string;
}
const {
title,
description = 'The fast, flexible & elegant library for parsing and manipulating HTML and XML.',
} = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<meta
name="keywords"
content="htmlparser, jquery, selector, scraper, parser, dom, xml, html, nodejs"
/>
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
<title>{title} | Cheerio</title>
<ViewTransitions />
</head>
<body
class="flex min-h-screen flex-col bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"
>
<Navbar />
<main class="flex-1">
<slot />
</main>
<Footer />
</body>
</html>

View File

@@ -0,0 +1,62 @@
---
import '@/styles/global.css';
import Navbar from '@/components/Navbar.astro';
import Footer from '@/components/Footer.astro';
interface Props {
title: string;
description?: string;
date?: Date;
author?: string;
}
const {
title,
description = 'The fast, flexible & elegant library for parsing and manipulating HTML and XML.',
date,
author,
} = Astro.props;
const formattedDate = date
? date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
: null;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
<title>{title} | Cheerio Blog</title>
</head>
<body
class="flex min-h-screen flex-col bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"
>
<Navbar />
<main class="flex-1">
<article class="mx-auto max-w-3xl px-4 py-12">
<header class="mb-8">
<h1 class="mb-4 text-4xl font-bold">{title}</h1>
{
(formattedDate || author) && (
<div class="flex items-center gap-4 text-slate-500 dark:text-slate-400">
{formattedDate && <time>{formattedDate}</time>}
{author && <span>by {author}</span>}
</div>
)
}
</header>
<div class="prose max-w-none">
<slot />
</div>
</article>
</main>
<Footer />
</body>
</html>

View File

@@ -0,0 +1,57 @@
---
import '@/styles/global.css';
import { ViewTransitions } from 'astro:transitions';
import Navbar from '@/components/Navbar.astro';
import Footer from '@/components/Footer.astro';
import Sidebar from '@/components/Sidebar.astro';
import TableOfContents from '@/components/TableOfContents.astro';
interface Props {
title: string;
description?: string;
headings?: Array<{
depth: number;
slug: string;
text: string;
}>;
}
const {
title,
description = 'The fast, flexible & elegant library for parsing and manipulating HTML and XML.',
headings = [],
} = Astro.props;
const currentPath = Astro.url.pathname;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<meta
name="keywords"
content="htmlparser, jquery, selector, scraper, parser, dom, xml, html, nodejs"
/>
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
<title>{title} | Cheerio</title>
<ViewTransitions />
</head>
<body
class="flex min-h-screen flex-col bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"
>
<Navbar />
<div class="mx-auto flex w-full max-w-8xl flex-1">
<Sidebar currentPath={currentPath} />
<main class="flex-1 min-w-0 px-10 py-10">
<article class="prose max-w-none">
<slot />
</article>
</main>
<TableOfContents headings={headings} />
</div>
<Footer />
</body>
</html>

View File

@@ -0,0 +1,30 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
---
<BaseLayout title="Attribution">
<div class="mx-auto max-w-3xl px-4 py-12">
<h1 class="mb-8 text-4xl font-bold">Attribution</h1>
<div class="mb-8 flex justify-center gap-8">
<img src="/img/1F57A.svg" alt="Dancing man emoji" class="h-32 w-32" />
<img src="/img/1F496.svg" alt="Sparkling heart emoji" class="h-32 w-32" />
<img src="/img/26A1.svg" alt="Lightning emoji" class="h-32 w-32" />
</div>
<div class="prose mx-auto">
<p>
Homepage images originate from <a
href="https://openmoji.org"
target="_blank"
rel="noopener noreferrer">OpenMoji</a
>, licensed under <a
href="https://creativecommons.org/licenses/by-sa/4.0/"
target="_blank"
rel="noopener noreferrer">CC BY-SA 4.0</a
>. The color of the dancing man emoji was changed to align with the rest
of the website.
</p>
</div>
</div>
</BaseLayout>

View File

@@ -1,17 +0,0 @@
import DancingMan from '../../static/img/1F57A.svg';
import SparklingHeart from '../../static/img/1F496.svg';
import Lightning from '../../static/img/26A1.svg';
# Attribution
<center>
<DancingMan height="200px" />
<SparklingHeart height="200px" />
<Lightning height="200px" />
</center>
Homepage images originate from [OpenMoji](https://openmoji.org), licensed under
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). The color of
the dancing man emoji was changed to align with the rest of the website.

View File

@@ -0,0 +1,33 @@
---
import { getCollection, render } from 'astro:content';
import BlogLayout from '@/layouts/BlogLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.id.replace('.md', '').replace('.mdx', '') },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
// Extract date from filename (e.g., "2024-08-07-version-1.md")
const dateParts = post.id.split('-').slice(0, 3);
const date = new Date(dateParts.join('-'));
// Get author name (simplified - in a real app you'd look this up)
const authorMap: Record<string, string> = {
fb55: 'Felix Boehm',
};
const authorName = post.data.authors
? (Array.isArray(post.data.authors)
? post.data.authors.map((a: string) => authorMap[a] || a).join(', ')
: authorMap[post.data.authors] || post.data.authors)
: undefined;
---
<BlogLayout title={post.data.title} date={date} author={authorName}>
<Content />
</BlogLayout>

View File

@@ -0,0 +1,118 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getCollection, render } from 'astro:content';
const posts = await getCollection('blog');
// Sort posts by date (newest first)
const sortedPosts = posts.sort((a, b) => {
const dateA = new Date(a.id.split('-').slice(0, 3).join('-'));
const dateB = new Date(b.id.split('-').slice(0, 3).join('-'));
return dateB.getTime() - dateA.getTime();
});
function getPostDate(id: string): string {
const datePart = id.split('-').slice(0, 3).join('-');
return new Date(datePart).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
function getPostSlug(id: string): string {
return id.replace('.md', '').replace('.mdx', '');
}
// Extract excerpt from raw markdown content (content before <!--truncate-->)
function getExcerpt(rawContent: string): string | null {
const truncateIndex = rawContent.indexOf('<!--truncate-->');
if (truncateIndex === -1) return null;
let excerpt = rawContent.slice(0, truncateIndex).trim();
// Remove frontmatter
const frontmatterEnd = excerpt.indexOf('---', 3);
if (excerpt.startsWith('---') && frontmatterEnd !== -1) {
excerpt = excerpt.slice(frontmatterEnd + 3).trim();
}
// Remove the first heading (usually the title, which duplicates frontmatter title)
excerpt = excerpt.replace(/^#\s+.+\n+/, '').trim();
// Remove admonition blocks (:::note, :::tip, etc.)
excerpt = excerpt.replace(/^:::\w+(\[.*?\])?\n[\s\S]*?^:::\n*/gm, '').trim();
return excerpt || null;
}
// Pre-render excerpts for each post
const postsWithExcerpts = sortedPosts.map((post) => {
const excerpt = getExcerpt(post.body || '');
return { post, excerpt };
});
---
<BaseLayout title="Blog">
<div class="mx-auto max-w-4xl px-4 py-12">
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-4xl font-bold">Blog</h1>
<p class="mt-2 text-lg text-slate-600 dark:text-slate-400">
Updates and announcements from the Cheerio team.
</p>
</div>
<a
href="/blog/rss.xml"
class="flex items-center gap-2 rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-600 transition-colors hover:bg-slate-100 dark:border-slate-700 dark:text-slate-400 dark:hover:bg-slate-800"
title="RSS Feed"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4">
<path d="M3.75 3a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H4c6.075 0 11 4.925 11 11v.25c0 .414.336.75.75.75h.5a.75.75 0 0 0 .75-.75V16C17 8.82 11.18 3 4 3h-.25Z" />
<path d="M3 8.75A.75.75 0 0 1 3.75 8H4a8 8 0 0 1 8 8v.25a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1-.75-.75V16a6 6 0 0 0-6-6h-.25A.75.75 0 0 1 3 9.25v-.5ZM7 15a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z" />
</svg>
RSS
</a>
</div>
<div class="space-y-8">
{
postsWithExcerpts.map(({ post, excerpt }) => (
<article class="rounded-lg border border-slate-200 p-6 transition-shadow hover:shadow-lg dark:border-slate-700">
<h2 class="mb-2 text-2xl font-semibold">
<a
href={`/blog/${getPostSlug(post.id)}`}
class="text-slate-900 transition-colors hover:text-primary dark:text-white"
>
{post.data.title}
</a>
</h2>
<div class="mb-4 flex items-center gap-4 text-sm text-slate-500 dark:text-slate-400">
<time>{getPostDate(post.id)}</time>
{post.data.tags && (
<div class="flex gap-2">
{post.data.tags.map((tag: string) => (
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs dark:bg-slate-800">
{tag}
</span>
))}
</div>
)}
</div>
{excerpt && (
<div class="prose prose-slate dark:prose-invert prose-sm max-w-none mb-4">
<p class="text-slate-600 dark:text-slate-400">{excerpt}</p>
</div>
)}
<a
href={`/blog/${getPostSlug(post.id)}`}
class="text-primary hover:underline font-medium"
>
Read more &rarr;
</a>
</article>
))
}
</div>
</div>
</BaseLayout>

View File

@@ -0,0 +1,39 @@
import rss from '@astrojs/rss';
import type { APIContext } from 'astro';
import { getCollection } from 'astro:content';
import { marked } from 'marked';
export async function GET(context: APIContext) {
const posts = await getCollection('blog');
// Sort posts by date (newest first)
const sortedPosts = posts.toSorted((a, b) => {
const dateA = new Date(a.id.split('-').slice(0, 3).join('-'));
const dateB = new Date(b.id.split('-').slice(0, 3).join('-'));
return dateB.getTime() - dateA.getTime();
});
if (!context.site) {
throw new Error('Site URL is required for RSS feed generation');
}
return rss({
title: 'Cheerio Blog',
description: 'Updates and announcements from the Cheerio team.',
site: context.site,
items: sortedPosts.map((post) => {
const datePart = post.id.split('-').slice(0, 3).join('-');
const slug = post.id.replace('.md', '').replace('.mdx', '');
// Render markdown body to HTML
const content = post.body ? marked.parse(post.body) : '';
return {
title: post.data.title,
pubDate: new Date(datePart),
link: `/blog/${slug}/`,
content: content as string,
};
}),
});
}

View File

@@ -0,0 +1,22 @@
---
import { getCollection, render } from 'astro:content';
import DocsLayout from '@/layouts/DocsLayout.astro';
export async function getStaticPaths() {
const docs = await getCollection('docs');
return docs
.filter((doc) => !doc.id.includes('/'))
.map((doc) => ({
params: { slug: doc.id.replace(/\.mdx?$/, '') },
props: { doc },
}));
}
const { doc } = Astro.props;
const { Content, headings } = await render(doc);
const title = doc.data.title || doc.data.sidebar_label || doc.id;
---
<DocsLayout title={title} description={doc.data.description} headings={headings}>
<Content />
</DocsLayout>

View File

@@ -0,0 +1,22 @@
---
import { getCollection, render } from 'astro:content';
import DocsLayout from '@/layouts/DocsLayout.astro';
export async function getStaticPaths() {
const docs = await getCollection('docs');
return docs
.filter((doc) => doc.id.startsWith('advanced/'))
.map((doc) => ({
params: { slug: doc.id.replace('advanced/', '').replace(/\.mdx?$/, '') },
props: { doc },
}));
}
const { doc } = Astro.props;
const { Content, headings } = await render(doc);
const title = doc.data.title || doc.data.sidebar_label || doc.id;
---
<DocsLayout title={title} description={doc.data.description} headings={headings}>
<Content />
</DocsLayout>

View File

@@ -0,0 +1,30 @@
---
import { getCollection, render } from 'astro:content';
import DocsLayout from '@/layouts/DocsLayout.astro';
export async function getStaticPaths() {
const docs = await getCollection('docs');
return docs
.filter((doc) => doc.id.startsWith('api/'))
.map((doc) => {
// Remove 'api/' prefix and file extension
let slug = doc.id.replace('api/', '').replace(/\.mdx?$/, '');
// Handle index page - should have undefined slug for [...slug] catch-all
if (slug === 'index') {
slug = undefined;
}
return {
params: { slug },
props: { doc },
};
});
}
const { doc } = Astro.props;
const { Content, headings } = await render(doc);
const title = doc.data.title || doc.data.sidebar_label || 'API Documentation';
---
<DocsLayout title={title} description={doc.data.description} headings={headings}>
<Content />
</DocsLayout>

View File

@@ -0,0 +1,22 @@
---
import { getCollection, render } from 'astro:content';
import DocsLayout from '@/layouts/DocsLayout.astro';
export async function getStaticPaths() {
const docs = await getCollection('docs');
return docs
.filter((doc) => doc.id.startsWith('basics/'))
.map((doc) => ({
params: { slug: doc.id.replace('basics/', '').replace(/\.mdx?$/, '') },
props: { doc },
}));
}
const { doc } = Astro.props;
const { Content, headings } = await render(doc);
const title = doc.data.title || doc.data.sidebar_label || doc.id;
---
<DocsLayout title={title} description={doc.data.description} headings={headings}>
<Content />
</DocsLayout>

View File

@@ -0,0 +1,23 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import Hero from '@/components/Hero.astro';
import Features from '@/components/Features.astro';
import Sponsors from '@/components/Sponsors.astro';
import Testimonials from '@/components/Testimonials.astro';
---
<BaseLayout title="The industry standard for working with HTML in JavaScript">
<Hero />
<Features />
<Sponsors />
<Testimonials />
<div class="mx-auto max-w-7xl px-4 py-8">
<a
href="/docs/intro"
class="block rounded-lg bg-primary py-4 text-center text-lg font-semibold text-white transition-colors hover:bg-primary-dark"
>
Learn more about Cheerio &rsaquo;
</a>
</div>
</BaseLayout>

View File

@@ -1,49 +0,0 @@
import React from 'react';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
// eslint-disable-next-line n/file-extension-in-import
import { HomepageFeatures } from '../components/homepage-features';
// eslint-disable-next-line n/file-extension-in-import
import { HomepageTweets } from '../components/homepage-tweets';
// eslint-disable-next-line n/file-extension-in-import
import { HeadlineSponsors } from '../components/homepage-sponsors';
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<header className="hero hero--primary padding-vert--xl text--center">
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<Link className="button button--secondary button--lg" to="/docs/intro">
Get Started!
</Link>
</div>
</header>
);
}
export default function Home(): JSX.Element {
const { siteConfig } = useDocusaurusContext();
return (
<Layout
title="The industry standard for working with HTML in JavaScript"
description={siteConfig.tagline}
>
<HomepageHeader />
<HomepageFeatures />
<HeadlineSponsors />
<HomepageTweets />
<div className="container">
<Link
className="button button--primary button--block margin-vert--lg padding-vert--md"
to="/docs/intro"
>
Learn more about Cheerio
</Link>
</div>
</Layout>
);
}

View File

@@ -0,0 +1,31 @@
import { visit } from 'unist-util-visit';
import type { Root, Element } from 'hast';
function visitExternalLink(node: Element): void {
if (
node.tagName === 'a' &&
node.properties?.href &&
typeof node.properties.href === 'string'
) {
const { href } = node.properties;
// Check if it's an external link (starts with http:// or https://)
if (href.startsWith('http://') || href.startsWith('https://')) {
node.properties.target = '_blank';
node.properties.rel = 'noopener noreferrer';
}
}
}
function transformer(tree: Root): void {
visit(tree, 'element', visitExternalLink);
}
/**
* Rehype plugin to make external links open in a new tab. Adds target="_blank"
* and rel="noopener noreferrer" to external links.
*
* @returns A transformer function.
*/
export function rehypeExternalLinks() {
return transformer;
}

View File

@@ -0,0 +1,84 @@
import { visit } from 'unist-util-visit';
import type { Root } from 'mdast';
interface DirectiveData {
hName?: string;
hProperties?: Record<string, string>;
directiveLabel?: boolean;
}
interface DirectiveNode {
type: string;
name: string;
data?: DirectiveData;
children: DirectiveChild[];
}
interface DirectiveChild {
type: string;
value?: string;
data?: DirectiveData;
children?: DirectiveChild[];
}
const ADMONITION_TYPES = ['note', 'tip', 'warning', 'danger', 'info'] as const;
function visitAdmonition(node: unknown): void {
const directive = node as DirectiveNode;
if (directive.type === 'containerDirective') {
if (
!ADMONITION_TYPES.includes(
directive.name as (typeof ADMONITION_TYPES)[number],
)
) {
return;
}
const data: DirectiveData = (directive.data ??= {});
/*
* Get title from the directive label or use default
* e.g., :::tip Title Here or just :::tip
*/
let title =
directive.name.charAt(0).toUpperCase() + directive.name.slice(1);
// Check if there's a custom title in the first text
const firstChild = directive.children[0];
if (firstChild?.data?.directiveLabel) {
title = firstChild.children?.[0]?.value ?? title;
// Remove the label paragraph from children
directive.children.shift();
}
data.hName = 'div';
data.hProperties = {
class: `admonition admonition-${directive.name}`,
'data-type': directive.name,
};
// Prepend a title element
directive.children.unshift({
type: 'paragraph',
data: {
hName: 'p',
hProperties: { class: 'admonition-title' },
},
children: [{ type: 'text', value: title }],
});
}
}
function transformer(tree: Root): void {
visit(tree, visitAdmonition);
}
/**
* Remark plugin to transform Docusaurus-style admonitions (:::note, :::tip,
* etc.) into custom HTML elements that can be styled with Tailwind.
*
* @returns A transformer function.
*/
export function remarkAdmonitions() {
return transformer;
}

View File

@@ -0,0 +1,23 @@
import { visit } from 'unist-util-visit';
import type { Root, Link } from 'mdast';
function visitTypedocLink(node: Link): void {
if (typeof node.url === 'string' && node.url.startsWith('/docs/api/')) {
// Remove .md extension from API doc links
node.url = node.url.replace(/\.md$/, '');
}
}
function transformer(tree: Root): void {
visit(tree, 'link', visitTypedocLink);
}
/**
* Remark plugin to fix typedoc-generated links. Removes .md extension from
* internal API doc links.
*
* @returns A transformer function.
*/
export function remarkFixTypedocLinks() {
return transformer;
}

View File

@@ -0,0 +1,73 @@
import { visit } from 'unist-util-visit';
import type { Root, Code, Parent } from 'mdast';
interface MdxJsxAttribute {
type: 'mdxJsxAttribute';
name: string;
value: string | null;
}
interface MdxJsxFlowElement {
type: 'mdxJsxFlowElement';
name: string;
attributes: MdxJsxAttribute[];
children: unknown[];
}
function visitLiveCode(
node: Code,
index: number | undefined,
parent: Parent | undefined,
): void {
// Check if the code block has 'live' in its meta
if (node.meta?.includes('live') && index !== undefined && parent) {
// Transform the code node into an MDX JSX element
const code = node.value;
/*
* Create an mdxJsxFlowElement node for the LiveCode component
* with client:visible for lazy hydration
*/
const jsxNode: MdxJsxFlowElement = {
type: 'mdxJsxFlowElement',
name: 'LiveCode',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'code',
value: code,
},
{
type: 'mdxJsxAttribute',
name: 'client:visible',
value: null,
},
],
children: [],
};
// Replace the code node with the JSX node
parent.children.splice(index, 1, jsxNode as unknown as Code);
}
}
function transformer(tree: Root): void {
visit(tree, 'code', visitLiveCode);
}
/**
* Remark plugin to transform code blocks with 'live' meta into LiveCode
* components.
*
* Usage in markdown:
*
* ```js
* const $ = cheerio.load('<h1>Hello</h1>');
* return <>{$('h1').text()}</>;
* ```
*
* @returns A transformer function.
*/
export function remarkLiveCode() {
return transformer;
}

View File

@@ -0,0 +1,262 @@
@import 'tailwindcss';
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url('/fonts/inter.woff') format('woff');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
U+FFFD;
}
@font-face {
font-family: 'Rubik';
font-style: normal;
font-weight: 400;
src: url('/fonts/rubik.woff') format('woff');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
U+FFFD;
}
@theme {
--color-primary: #e88c1f;
--color-primary-dark: #d77e16;
--color-primary-light: #ea9837;
--color-primary-lightest: #efb167;
--font-sans:
'Rubik', system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell,
'Noto Sans', sans-serif;
--font-heading: 'Inter', var(--font-sans);
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-sans);
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-heading);
font-weight: 600;
}
/* Prose styles for documentation */
.prose {
max-width: 65ch;
}
.prose h1,
.prose h2,
.prose h3,
.prose h4 {
font-family: var(--font-heading);
font-weight: 600;
margin-top: 1.5em;
margin-bottom: 0.5em;
}
.prose h1 {
font-size: 2.25rem;
}
.prose h2 {
font-size: 1.75rem;
}
.prose h3 {
font-size: 1.375rem;
}
.prose p {
margin-top: 1em;
margin-bottom: 1em;
}
.prose a {
color: var(--color-primary);
text-decoration: underline;
}
.prose a:hover {
color: var(--color-primary-dark);
}
.prose code {
background-color: #f1f5f9;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-size: 0.875em;
}
.prose pre {
background-color: #1e293b;
color: #e2e8f0;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.prose pre code {
background-color: transparent;
padding: 0;
font-size: 0.875rem;
}
.prose ul,
.prose ol {
margin-top: 1em;
margin-bottom: 1em;
padding-left: 1.5em;
}
.prose li {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
.prose blockquote {
border-left: 4px solid var(--color-primary);
padding-left: 1rem;
margin: 1rem 0;
color: #64748b;
font-style: italic;
}
/* Admonition styles */
.admonition {
padding: 1rem;
border-radius: 0.5rem;
margin: 1.5rem 0;
}
.admonition-title {
font-weight: 600;
margin: 0 0 0.5rem 0 !important;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
}
.admonition-note {
background-color: #eff6ff;
border-left: 4px solid #3b82f6;
}
.admonition-note .admonition-title {
color: #1d4ed8;
}
.admonition-tip {
background-color: #f0fdf4;
border-left: 4px solid #22c55e;
}
.admonition-tip .admonition-title {
color: #15803d;
}
.admonition-info {
background-color: #f0f9ff;
border-left: 4px solid #0ea5e9;
}
.admonition-info .admonition-title {
color: #0369a1;
}
.admonition-warning {
background-color: #fefce8;
border-left: 4px solid #eab308;
}
.admonition-warning .admonition-title {
color: #a16207;
}
.admonition-danger {
background-color: #fef2f2;
border-left: 4px solid #ef4444;
}
.admonition-danger .admonition-title {
color: #b91c1c;
}
.admonition p:last-child {
margin-bottom: 0;
}
.admonition code {
background-color: rgba(0, 0, 0, 0.1);
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--color-primary: #f4a02f;
--color-primary-dark: #f39313;
--color-primary-light: #f5ad4b;
}
.prose code {
background-color: #334155;
color: #e2e8f0;
}
.admonition-note {
background-color: #1e3a5f;
}
.admonition-note .admonition-title {
color: #60a5fa;
}
.admonition-tip {
background-color: #14532d;
}
.admonition-tip .admonition-title {
color: #4ade80;
}
.admonition-info {
background-color: #0c4a6e;
}
.admonition-info .admonition-title {
color: #38bdf8;
}
.admonition-warning {
background-color: #422006;
}
.admonition-warning .admonition-title {
color: #fbbf24;
}
.admonition-danger {
background-color: #450a0a;
}
.admonition-danger .admonition-title {
color: #f87171;
}
.admonition code {
background-color: rgba(255, 255, 255, 0.1);
}
}

View File

@@ -1,11 +0,0 @@
import React from 'react';
import * as cheerio from '../../../../dist/browser/index.js';
// Add react-live imports you need here
const ReactLiveScope = {
cheerio,
React,
...React,
};
export default ReactLiveScope;

View File

@@ -1,11 +1,15 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"jsx": "react-jsx",
"jsxImportSource": "react",
"resolveJsonModule": true,
"checkJs": true,
"strict": true
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"exclude": [".docusaurus", "build", "node_modules"]
"include": ["src", ".astro", "*.json", "*.mjs"]
}

36
website/typedoc.json Normal file
View File

@@ -0,0 +1,36 @@
{
"$schema": "https://typedoc.org/schema.json",
"entryPoints": ["../src/index.ts"],
"tsconfig": "../tsconfig.typedoc.json",
"readme": "none",
"excludePrivate": true,
"plugin": ["typedoc-plugin-markdown", "typedoc-plugin-mdn-links"],
"out": "src/content/docs/api",
"entryFileName": "index",
"router": "kind",
"publicPath": "/docs/api/",
"fileExtension": ".md",
"hidePageHeader": true,
"hideBreadcrumbs": true,
"useCodeBlocks": true,
"expandObjects": true,
"parametersFormat": "table",
"enumMembersFormat": "table",
"typeDeclarationFormat": "table",
"indexFormat": "table",
"sanitizeComments": true,
"externalSymbolLinkMappings": {
"domhandler": {
"Document": "https://domhandler.js.org/classes/Document.html",
"Element": "https://domhandler.js.org/classes/Element.html",
"Node": "https://domhandler.js.org/classes/Node.html",
"ChildNode": "https://domhandler.js.org/types/ChildNode.html",
"ParentNode": "https://domhandler.js.org/types/ParentNode.html",
"AnyNode": "https://domhandler.js.org/types/AnyNode.html"
},
"parse5": {
"TreeAdapterTypeMap": "https://parse5.js.org/interfaces/parse5.TreeAdapterTypeMap.html",
"TreeAdapter": "https://parse5.js.org/interfaces/parse5.TreeAdapter.html"
}
}
}