diff options
Diffstat (limited to 'src/utils')
| -rw-r--r-- | src/utils/helpers/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/helpers/pages.tsx | 8 | ||||
| -rw-r--r-- | src/utils/helpers/rehype.ts | 23 | ||||
| -rw-r--r-- | src/utils/helpers/rss.ts | 4 | ||||
| -rw-r--r-- | src/utils/hooks/use-article.ts | 18 | ||||
| -rw-r--r-- | src/utils/hooks/use-headings-tree/use-headings-tree.test.ts | 47 | ||||
| -rw-r--r-- | src/utils/hooks/use-headings-tree/use-headings-tree.ts | 36 | ||||
| -rw-r--r-- | src/utils/hooks/use-posts-list/use-posts-list.ts | 11 |
8 files changed, 99 insertions, 49 deletions
diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 79077de..92f9424 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -3,6 +3,7 @@ export * from './images'; export * from './pages'; export * from './reading-time'; export * from './refs'; +export * from './rehype'; export * from './rss'; export * from './schema-org'; export * from './strings'; diff --git a/src/utils/helpers/pages.tsx b/src/utils/helpers/pages.tsx index 62a582f..7b6bdca 100644 --- a/src/utils/helpers/pages.tsx +++ b/src/utils/helpers/pages.tsx @@ -109,9 +109,9 @@ export const getPostsWithUrl = (posts: Article[]): PostData[] => * @param {EdgesResponse<RawArticle>[]} rawData - The raw data. * @returns {PostData[]} An array of posts. */ -export const getPostsList = ( +export const getPostsList = async ( rawData: EdgesResponse<RawArticle>[] -): PostData[] => { +): Promise<PostData[]> => { const articlesList: RawArticle[] = []; rawData.forEach((articleData) => { articleData.edges.forEach((edge) => { @@ -120,6 +120,8 @@ export const getPostsList = ( }); return getPostsWithUrl( - articlesList.map((article) => getArticleFromRawData(article)) + await Promise.all( + articlesList.map(async (article) => getArticleFromRawData(article)) + ) ); }; diff --git a/src/utils/helpers/rehype.ts b/src/utils/helpers/rehype.ts new file mode 100644 index 0000000..2716c62 --- /dev/null +++ b/src/utils/helpers/rehype.ts @@ -0,0 +1,23 @@ +/** + * Update a stringified HTML tree using unified plugins. + * + * It will parse the provided content to add id to each headings. + * + * @param {string} content - The page contents. + * @returns {string} The updated page contents. + */ +export const updateContentTree = async (content: string): Promise<string> => { + const { unified } = await import('unified'); + const rehypeParse = (await import('rehype-parse')).default; + const rehypeSanitize = (await import('rehype-sanitize')).default; + const rehypeSlug = (await import('rehype-slug')).default; + const rehypeStringify = (await import('rehype-stringify')).default; + + return unified() + .use(rehypeParse, { fragment: true }) + .use(rehypeSlug) + .use(() => rehypeSanitize({ clobberPrefix: 'h-' })) + .use(rehypeStringify) + .processSync(content) + .toString(); +}; diff --git a/src/utils/helpers/rss.ts b/src/utils/helpers/rss.ts index 6de60cc..d9c3b1e 100644 --- a/src/utils/helpers/rss.ts +++ b/src/utils/helpers/rss.ts @@ -18,8 +18,8 @@ const getAllArticles = async (): Promise<Article[]> => { const rawArticles = await getArticles({ first: totalArticles }); const articles: Article[] = []; - rawArticles.edges.forEach((edge) => { - articles.push(getArticleFromRawData(edge.node)); + rawArticles.edges.forEach(async (edge) => { + articles.push(await getArticleFromRawData(edge.node)); }); return articles; diff --git a/src/utils/hooks/use-article.ts b/src/utils/hooks/use-article.ts index 5cf0e51..f339f7f 100644 --- a/src/utils/hooks/use-article.ts +++ b/src/utils/hooks/use-article.ts @@ -1,10 +1,11 @@ +import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { articleBySlugQuery, fetchAPI, getArticleFromRawData, } from '../../services/graphql'; -import type { Article, RawArticle } from '../../types'; +import type { Article, Maybe, RawArticle } from '../../types'; export type UseArticleConfig = { /** @@ -32,6 +33,19 @@ export const useArticle = ({ fetchAPI<RawArticle, typeof articleBySlugQuery>, {} ); + const [article, setArticle] = useState<Maybe<Article>>(); - return data ? getArticleFromRawData(data.post) : fallback; + useEffect(() => { + const getArticle = async () => { + if (data) { + setArticle(await getArticleFromRawData(data.post)); + } else { + setArticle(fallback); + } + }; + + getArticle(); + }, [data, fallback]); + + return article; }; diff --git a/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts b/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts index ad30a4f..2c8ff2d 100644 --- a/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts +++ b/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals'; -import { renderHook } from '@testing-library/react'; +import { act, renderHook } from '@testing-library/react'; import { useHeadingsTree } from './use-headings-tree'; const labels = { @@ -9,7 +9,7 @@ const labels = { }; describe('useHeadingsTree', () => { - it('returns a ref object and the headings tree', () => { + it('returns a ref callback and the headings tree', () => { const wrapper = document.createElement('div'); wrapper.innerHTML = ` @@ -19,12 +19,13 @@ describe('useHeadingsTree', () => { <h2>${labels.secondH2}</h2> <p>Totam cumque aut ipsum. Necessitatibus magnam necessitatibus. Qui illo nulla non ab. Accusamus voluptatem ab fugiat voluptas aspernatur velit dolore reprehenderit. Voluptatem quod minima asperiores voluptatum distinctio cumque quo.</p>`; - const wrapperRef = { current: wrapper }; - const { result } = renderHook(() => useHeadingsTree(wrapperRef)); + const { result } = renderHook(() => useHeadingsTree()); - expect(result.current.length).toBe(1); - expect(result.current[0].label).toBe(labels.h1); - expect(result.current[0].children.length).toBe(2); + act(() => result.current.ref(wrapper)); + + expect(result.current.tree.length).toBe(1); + expect(result.current.tree[0].label).toBe(labels.h1); + expect(result.current.tree[0].children.length).toBe(2); }); it('can return a headings tree starting at the specified level', () => { @@ -37,14 +38,13 @@ describe('useHeadingsTree', () => { <h2>${labels.secondH2}</h2> <p>Totam cumque aut ipsum. Necessitatibus magnam necessitatibus. Qui illo nulla non ab. Accusamus voluptatem ab fugiat voluptas aspernatur velit dolore reprehenderit. Voluptatem quod minima asperiores voluptatum distinctio cumque quo.</p>`; - const wrapperRef = { current: wrapper }; - const { result } = renderHook(() => - useHeadingsTree(wrapperRef, { fromLevel: 2 }) - ); + const { result } = renderHook(() => useHeadingsTree({ fromLevel: 2 })); + + act(() => result.current.ref(wrapper)); - expect(result.current.length).toBe(2); - expect(result.current[0].label).toBe(labels.firstH2); - expect(result.current[1].label).toBe(labels.secondH2); + expect(result.current.tree.length).toBe(2); + expect(result.current.tree[0].label).toBe(labels.firstH2); + expect(result.current.tree[1].label).toBe(labels.secondH2); }); it('can return a headings tree stopping at the specified level', () => { @@ -57,22 +57,17 @@ describe('useHeadingsTree', () => { <h2>${labels.secondH2}</h2> <p>Totam cumque aut ipsum. Necessitatibus magnam necessitatibus. Qui illo nulla non ab. Accusamus voluptatem ab fugiat voluptas aspernatur velit dolore reprehenderit. Voluptatem quod minima asperiores voluptatum distinctio cumque quo.</p>`; - const wrapperRef = { current: wrapper }; - const { result } = renderHook(() => - useHeadingsTree(wrapperRef, { toLevel: 1 }) - ); + const { result } = renderHook(() => useHeadingsTree({ toLevel: 1 })); + + act(() => result.current.ref(wrapper)); - expect(result.current.length).toBe(1); - expect(result.current[0].label).toBe(labels.h1); - expect(result.current[0].children).toStrictEqual([]); + expect(result.current.tree.length).toBe(1); + expect(result.current.tree[0].label).toBe(labels.h1); + expect(result.current.tree[0].children).toStrictEqual([]); }); it('throws an error if the options are invalid', () => { - const wrapperRef = { current: null }; - - expect(() => - useHeadingsTree(wrapperRef, { fromLevel: 2, toLevel: 1 }) - ).toThrowError( + expect(() => useHeadingsTree({ fromLevel: 2, toLevel: 1 })).toThrowError( 'Invalid options: `fromLevel` must be lower or equal to `toLevel`.' ); }); diff --git a/src/utils/hooks/use-headings-tree/use-headings-tree.ts b/src/utils/hooks/use-headings-tree/use-headings-tree.ts index 6a081e7..68bdde8 100644 --- a/src/utils/hooks/use-headings-tree/use-headings-tree.ts +++ b/src/utils/hooks/use-headings-tree/use-headings-tree.ts @@ -1,4 +1,4 @@ -import { useEffect, useState, type RefObject } from 'react'; +import { useState, useCallback, type RefCallback } from 'react'; import type { HeadingLevel } from '../../../components'; export type HeadingsTreeNode = { @@ -111,17 +111,26 @@ const buildHeadingsTreeFrom = ( return treeNodes; }; +export type UseHeadingsTreeReturn<T extends HTMLElement> = { + /** + * A callback function to set a ref. + */ + ref: RefCallback<T>; + /** + * The headings tree. + */ + tree: HeadingsTreeNode[]; +}; + /** * React hook to retrieve the headings tree in a document or in a given wrapper. * - * @param {RefObject<T>} ref - A ref to the element where to look for headings. * @param {UseHeadingsTreeOptions} options - The headings tree config. - * @returns {HeadingsTreeNode[]} The headings tree. + * @returns {UseHeadingsTreeReturn<T>} The headings tree and a ref callback. */ export const useHeadingsTree = <T extends HTMLElement = HTMLElement>( - ref: RefObject<T>, options?: UseHeadingsTreeOptions -): HeadingsTreeNode[] => { +): UseHeadingsTreeReturn<T> => { if ( options?.fromLevel && options.toLevel && @@ -134,15 +143,14 @@ export const useHeadingsTree = <T extends HTMLElement = HTMLElement>( const [tree, setTree] = useState<HeadingsTreeNode[]>([]); const requestedHeadingTags = getHeadingTagsList(options); const query = requestedHeadingTags.join(', '); + const ref: RefCallback<T> = useCallback( + (el) => { + const headingNodes = el?.querySelectorAll<HTMLHeadingElement>(query); - useEffect(() => { - if (typeof window === 'undefined') return; - - const headingNodes = - ref.current?.querySelectorAll<HTMLHeadingElement>(query); - - if (headingNodes) setTree(buildHeadingsTreeFrom(headingNodes)); - }, [query, ref]); + if (headingNodes) setTree(buildHeadingsTreeFrom(headingNodes)); + }, + [query] + ); - return tree; + return { ref, tree }; }; diff --git a/src/utils/hooks/use-posts-list/use-posts-list.ts b/src/utils/hooks/use-posts-list/use-posts-list.ts index 661727f..980d531 100644 --- a/src/utils/hooks/use-posts-list/use-posts-list.ts +++ b/src/utils/hooks/use-posts-list/use-posts-list.ts @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import type { PostData } from '../../../components'; import type { Maybe, RawArticle } from '../../../types'; import { getPostsList } from '../../helpers'; @@ -40,8 +40,15 @@ export const usePostsList = ( } = usePagination(config); const [firstNewResultIndex, setFirstNewResultIndex] = useState<Maybe<number>>(undefined); + const [posts, setPosts] = useState<Maybe<PostData[]>>(undefined); - const posts = data ? getPostsList(data) : undefined; + useEffect(() => { + const getPosts = async () => { + if (data) setPosts(await getPostsList(data)); + }; + + getPosts(); + }, [data]); const handleLoadMore = useCallback(async () => { setFirstNewResultIndex(size * config.perPage + 1); |
