From b7ad77d73bbee67a683109df78813602ec964b36 Mon Sep 17 00:00:00 2001 From: GauthierWebDev Date: Fri, 11 Apr 2025 12:39:57 +0200 Subject: [PATCH] feat: Add estimated reading time to DocsHeader and DocsLayout --- app/components/syntax/DocsHeader.tsx | 19 +++++- app/components/syntax/DocsLayout.tsx | 4 +- app/markdoc/{nodes.ts => nodes.tsx} | 1 + app/package.json | 2 + app/pages/docs/+Page.tsx | 4 +- app/pages/docs/+data.ts | 5 +- app/pages/star-wars/@id/+Page.tsx | 17 ----- app/pages/star-wars/@id/+data.ts | 32 --------- app/pages/star-wars/index/+Page.tsx | 22 ------- app/pages/star-wars/index/+data.ts | 32 --------- app/pages/star-wars/types.ts | 10 --- app/pages/todo/+Page.tsx | 14 ---- app/pages/todo/+config.ts | 3 - app/pages/todo/+data.ts | 11 ---- app/pages/todo/TodoList.telefunc.ts | 7 -- app/pages/todo/TodoList.tsx | 52 --------------- app/pnpm-lock.yaml | 98 ++++++++++++++++++++++++++++ 17 files changed, 125 insertions(+), 208 deletions(-) rename app/markdoc/{nodes.ts => nodes.tsx} (95%) delete mode 100644 app/pages/star-wars/@id/+Page.tsx delete mode 100644 app/pages/star-wars/@id/+data.ts delete mode 100644 app/pages/star-wars/index/+Page.tsx delete mode 100644 app/pages/star-wars/index/+data.ts delete mode 100644 app/pages/star-wars/types.ts delete mode 100644 app/pages/todo/+Page.tsx delete mode 100644 app/pages/todo/+config.ts delete mode 100644 app/pages/todo/+data.ts delete mode 100644 app/pages/todo/TodoList.telefunc.ts delete mode 100644 app/pages/todo/TodoList.tsx diff --git a/app/components/syntax/DocsHeader.tsx b/app/components/syntax/DocsHeader.tsx index 00bdb7c..7ab57ea 100644 --- a/app/components/syntax/DocsHeader.tsx +++ b/app/components/syntax/DocsHeader.tsx @@ -1,19 +1,32 @@ import { usePageContext } from "vike-react/usePageContext"; +import { ClockIcon } from "@heroicons/react/24/outline"; import { navigation } from "@/lib/navigation"; -export function DocsHeader({ title }: { title?: string }) { +type DocsHeaderProps = { + title?: string; + estimatedReadingTime?: string; +}; + +export function DocsHeader(props: DocsHeaderProps) { const { urlPathname } = usePageContext(); const section = navigation.find((section) => section.links.find((link) => link.href === urlPathname)); - if (!title && !section) { + if (!props.title && !section) { return null; } return (
{section &&

{section.title}

} - {title &&

{title}

} + {props.title && ( +

{props.title}

+ )} + {props.estimatedReadingTime && ( +

+ {props.estimatedReadingTime} +

+ )}
); } diff --git a/app/components/syntax/DocsLayout.tsx b/app/components/syntax/DocsLayout.tsx index 082034d..0e2f85a 100644 --- a/app/components/syntax/DocsLayout.tsx +++ b/app/components/syntax/DocsLayout.tsx @@ -9,10 +9,12 @@ import { Prose } from "@syntax/Prose"; export function DocsLayout({ children, frontmatter: { title }, + estimatedReadingTime, nodes, }: { children: React.ReactNode; frontmatter: { title?: string }; + estimatedReadingTime?: string; nodes: Array; }) { let tableOfContents = collectSections(nodes); @@ -21,7 +23,7 @@ export function DocsLayout({ <>
- + {children}
diff --git a/app/markdoc/nodes.ts b/app/markdoc/nodes.tsx similarity index 95% rename from app/markdoc/nodes.ts rename to app/markdoc/nodes.tsx index 4eb69fb..95afbe8 100644 --- a/app/markdoc/nodes.ts +++ b/app/markdoc/nodes.tsx @@ -18,6 +18,7 @@ const nodes = { this.render, { frontmatter: yaml.load(node.attributes.frontmatter), + estimatedReadingTime: config?.variables?.estimatedReadingTime, nodes: node.children, }, node.transformChildren(config), diff --git a/app/package.json b/app/package.json index 105c918..44479a4 100644 --- a/app/package.json +++ b/app/package.json @@ -12,6 +12,7 @@ "@fontsource-variable/inter": "^5.2.5", "@fontsource-variable/lexend": "^5.2.5", "@headlessui/react": "^2.2.0", + "@heroicons/react": "^2.2.0", "@markdoc/markdoc": "^0.5.1", "@sindresorhus/slugify": "^2.2.1", "@tailwindcss/typography": "^0.5.16", @@ -28,6 +29,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-highlight-words": "^0.21.0", + "reading-time-estimator": "^1.12.0", "simple-functional-loader": "^1.2.1", "telefunc": "^0.1.87", "unplugin-fonts": "^1.3.1", diff --git a/app/pages/docs/+Page.tsx b/app/pages/docs/+Page.tsx index 43ffd68..a675f16 100644 --- a/app/pages/docs/+Page.tsx +++ b/app/pages/docs/+Page.tsx @@ -7,10 +7,10 @@ import tags from "@/markdoc/tags"; import React from "react"; export default function Page() { - const { doc } = useData(); + const { doc, estimatedReadingTime } = useData(); const parsedDoc = Markdoc.parse(doc.content); - const transformedDoc = Markdoc.transform(parsedDoc, { nodes, tags, variables: {} }); + const transformedDoc = Markdoc.transform(parsedDoc, { nodes, tags, variables: { estimatedReadingTime } }); return Markdoc.renderers.react(transformedDoc, React); } diff --git a/app/pages/docs/+data.ts b/app/pages/docs/+data.ts index c0c3613..6cde874 100644 --- a/app/pages/docs/+data.ts +++ b/app/pages/docs/+data.ts @@ -1,6 +1,7 @@ import type { PageContext } from "vike/types"; import { docsService } from "@/services/DocsService"; +import { readingTime } from "reading-time-estimator"; import { useConfig } from "vike-react/useConfig"; import buildTitle from "@/pages/buildTitle"; import { render } from "vike/abort"; @@ -18,7 +19,7 @@ export async function data(pageContext: PageContext) { throw render(404); } - console.log({ doc }); + const readingTimeObject = readingTime(doc.content, 300, "fr"); config({ title: buildTitle(doc.title), @@ -27,5 +28,5 @@ export async function data(pageContext: PageContext) { docsService.transform(doc); - return { doc }; + return { doc, estimatedReadingTime: readingTimeObject.text }; } diff --git a/app/pages/star-wars/@id/+Page.tsx b/app/pages/star-wars/@id/+Page.tsx deleted file mode 100644 index 6e50b0e..0000000 --- a/app/pages/star-wars/@id/+Page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; -import { useData } from "vike-react/useData"; -import type { Data } from "./+data.js"; - -export default function Page() { - const movie = useData(); - return ( - <> -

{movie.title}

- Release Date: {movie.release_date} -
- Director: {movie.director} -
- Producer: {movie.producer} - - ); -} diff --git a/app/pages/star-wars/@id/+data.ts b/app/pages/star-wars/@id/+data.ts deleted file mode 100644 index 3b5a342..0000000 --- a/app/pages/star-wars/@id/+data.ts +++ /dev/null @@ -1,32 +0,0 @@ -// https://vike.dev/data - -import type { PageContextServer } from "vike/types"; -import type { MovieDetails } from "../types.js"; -import { useConfig } from "vike-react/useConfig"; - -export type Data = Awaited>; - -export const data = async (pageContext: PageContextServer) => { - // https://vike.dev/useConfig - const config = useConfig(); - - const response = await fetch(`https://brillout.github.io/star-wars/api/films/${pageContext.routeParams.id}.json`); - let movie = (await response.json()) as MovieDetails; - - config({ - // Set - title: movie.title, - }); - - // We remove data we don't need because the data is passed to - // the client; we should minimize what is sent over the network. - movie = minimize(movie); - - return movie; -}; - -function minimize(movie: MovieDetails): MovieDetails { - const { id, title, release_date, director, producer } = movie; - const minimizedMovie = { id, title, release_date, director, producer }; - return minimizedMovie; -} diff --git a/app/pages/star-wars/index/+Page.tsx b/app/pages/star-wars/index/+Page.tsx deleted file mode 100644 index 3c24a4e..0000000 --- a/app/pages/star-wars/index/+Page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; -import { useData } from "vike-react/useData"; -import type { Data } from "./+data.js"; - -export default function Page() { - const movies = useData<Data>(); - return ( - <> - <h1>Star Wars Movies</h1> - <ol> - {movies.map(({ id, title, release_date }) => ( - <li key={id}> - <a href={`/star-wars/${id}`}>{title}</a> ({release_date}) - </li> - ))} - </ol> - <p> - Source: <a href="https://brillout.github.io/star-wars">brillout.github.io/star-wars</a>. - </p> - </> - ); -} diff --git a/app/pages/star-wars/index/+data.ts b/app/pages/star-wars/index/+data.ts deleted file mode 100644 index 73834ea..0000000 --- a/app/pages/star-wars/index/+data.ts +++ /dev/null @@ -1,32 +0,0 @@ -// https://vike.dev/data - -import type { Movie, MovieDetails } from "../types.js"; -import { useConfig } from "vike-react/useConfig"; - -export type Data = Awaited<ReturnType<typeof data>>; - -export const data = async () => { - // https://vike.dev/useConfig - const config = useConfig(); - - const response = await fetch("https://brillout.github.io/star-wars/api/films.json"); - const moviesData = (await response.json()) as MovieDetails[]; - - config({ - // Set <title> - title: `${moviesData.length} Star Wars Movies`, - }); - - // We remove data we don't need because the data is passed to the client; we should - // minimize what is sent over the network. - const movies = minimize(moviesData); - - return movies; -}; - -function minimize(movies: MovieDetails[]): Movie[] { - return movies.map((movie) => { - const { title, release_date, id } = movie; - return { title, release_date, id }; - }); -} diff --git a/app/pages/star-wars/types.ts b/app/pages/star-wars/types.ts deleted file mode 100644 index ffccdf5..0000000 --- a/app/pages/star-wars/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type Movie = { - id: string; - title: string; - release_date: string; -}; - -export type MovieDetails = Movie & { - director: string; - producer: string; -}; diff --git a/app/pages/todo/+Page.tsx b/app/pages/todo/+Page.tsx deleted file mode 100644 index 674b784..0000000 --- a/app/pages/todo/+Page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { Data } from "./+data"; -import React from "react"; -import { useData } from "vike-react/useData"; -import { TodoList } from "./TodoList.js"; - -export default function Page() { - const data = useData<Data>(); - return ( - <> - <h1>To-do List</h1> - <TodoList initialTodoItems={data.todo} /> - </> - ); -} diff --git a/app/pages/todo/+config.ts b/app/pages/todo/+config.ts deleted file mode 100644 index a668c0a..0000000 --- a/app/pages/todo/+config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const config = { - prerender: false, -}; diff --git a/app/pages/todo/+data.ts b/app/pages/todo/+data.ts deleted file mode 100644 index 60954bf..0000000 --- a/app/pages/todo/+data.ts +++ /dev/null @@ -1,11 +0,0 @@ -// https://vike.dev/data -import { todos } from "../../database/todoItems"; -import type { PageContextServer } from "vike/types"; - -export type Data = { - todo: { text: string }[]; -}; - -export default async function data(_pageContext: PageContextServer): Promise<Data> { - return { todo: todos }; -} diff --git a/app/pages/todo/TodoList.telefunc.ts b/app/pages/todo/TodoList.telefunc.ts deleted file mode 100644 index abb5d74..0000000 --- a/app/pages/todo/TodoList.telefunc.ts +++ /dev/null @@ -1,7 +0,0 @@ -// We use Telefunc (https://telefunc.com) for data mutations. Being able to use Telefunc for fetching initial data is work-in-progress (https://vike.dev/data-fetching#tools). - -import { todos } from "../../database/todoItems"; - -export async function onNewTodo({ text }: { text: string }) { - todos.push({ text }); -} diff --git a/app/pages/todo/TodoList.tsx b/app/pages/todo/TodoList.tsx deleted file mode 100644 index 0b3483c..0000000 --- a/app/pages/todo/TodoList.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { onNewTodo } from "./TodoList.telefunc"; -import React, { useState } from "react"; - -export function TodoList({ initialTodoItems }: { initialTodoItems: { text: string }[] }) { - const [todoItems, setTodoItems] = useState(initialTodoItems); - const [newTodo, setNewTodo] = useState(""); - return ( - <> - <ul> - {todoItems.map((todoItem, index) => ( - // biome-ignore lint: - <li key={index}>{todoItem.text}</li> - ))} - </ul> - <div> - <form - onSubmit={async (ev) => { - ev.preventDefault(); - - // Optimistic UI update - setTodoItems((prev) => [...prev, { text: newTodo }]); - try { - await onNewTodo({ text: newTodo }); - setNewTodo(""); - } catch (e) { - console.error(e); - // rollback - setTodoItems((prev) => prev.slice(0, -1)); - } - }} - > - <input - type="text" - onChange={(ev) => setNewTodo(ev.target.value)} - value={newTodo} - className={ - "bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-purple-500 focus:border-purple-500 w-full sm:w-auto p-2 mr-1 mb-1" - } - /> - <button - type="submit" - className={ - "text-white bg-purple-700 hover:bg-purple-800 focus:ring-2 focus:outline-hidden focus:ring-purple-300 font-medium rounded-lg text-sm w-full sm:w-auto p-2" - } - > - Add to-do - </button> - </form> - </div> - </> - ); -} diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 52d3206..e86f18a 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@headlessui/react': specifier: ^2.2.0 version: 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@heroicons/react': + specifier: ^2.2.0 + version: 2.2.0(react@19.0.0) '@markdoc/markdoc': specifier: ^0.5.1 version: 0.5.1(@types/react@19.0.10)(react@19.0.0) @@ -74,6 +77,9 @@ importers: react-highlight-words: specifier: ^0.21.0 version: 0.21.0(react@19.0.0) + reading-time-estimator: + specifier: ^1.12.0 + version: 1.12.0 simple-functional-loader: specifier: ^1.2.1 version: 1.2.1 @@ -719,6 +725,11 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + '@heroicons/react@2.2.0': + resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} + peerDependencies: + react: '>= 16 || ^19.0.0-rc' + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1401,6 +1412,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1425,6 +1440,19 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1449,6 +1477,10 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + es-abstract@1.23.9: resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} engines: {node: '>= 0.4'} @@ -1772,6 +1804,9 @@ packages: highlight-words-core@1.2.3: resolution: {integrity: sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ==} + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -1867,6 +1902,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2180,6 +2219,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2310,6 +2352,9 @@ packages: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} + reading-time-estimator@1.12.0: + resolution: {integrity: sha512-A17wehSIG6bxen84mf5m+MP4bDDZytcfMOfJWDp1DT8StGRgljtD58zC6E+fSnQZvU5mfsjiJZ7BLhr9kHYoQQ==} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -2385,6 +2430,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sanitize-html@2.15.0: + resolution: {integrity: sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==} + scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} @@ -3298,6 +3346,10 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@heroicons/react@2.2.0(react@19.0.0)': + dependencies: + react: 19.0.0 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -4007,6 +4059,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -4029,6 +4083,24 @@ snapshots: dependencies: esutils: 2.0.3 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4050,6 +4122,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + entities@4.5.0: {} + es-abstract@1.23.9: dependencies: array-buffer-byte-length: 1.0.2 @@ -4535,6 +4609,13 @@ snapshots: highlight-words-core@1.2.3: {} + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -4634,6 +4715,8 @@ snapshots: is-number@7.0.0: {} + is-plain-object@5.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -4918,6 +5001,8 @@ snapshots: dependencies: callsites: 3.1.0 + parse-srcset@1.0.2: {} + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -5043,6 +5128,10 @@ snapshots: react@19.0.0: {} + reading-time-estimator@1.12.0: + dependencies: + sanitize-html: 2.15.0 + real-require@0.2.0: {} reflect.getprototypeof@1.0.10: @@ -5143,6 +5232,15 @@ snapshots: safer-buffer@2.1.2: {} + sanitize-html@2.15.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.5.3 + scheduler@0.25.0: {} search-insights@2.17.3: {}