renderer.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import { useCallback, useEffect, useRef } from 'react';
  2. import type { HtmlElementNode } from 'rehype-toc';
  3. import useSWR, { type SWRConfiguration, type SWRResponse } from 'swr';
  4. import { getGrowiFacade } from '~/features/growi-plugin/client/utils/growi-facade-utils';
  5. import type { RendererOptions } from '~/interfaces/renderer-options';
  6. import type { RendererConfigExt } from '~/interfaces/services/renderer';
  7. import { useCurrentPagePath } from '~/states/page';
  8. import { useRendererConfig } from '~/states/server-configurations';
  9. import { useSetTocNode } from '~/states/ui/toc';
  10. import { useNextThemes } from '~/stores-universal/use-next-themes';
  11. import loggerFactory from '~/utils/logger';
  12. const logger = loggerFactory('growi:cli:services:renderer');
  13. const useRendererConfigExt = (): RendererConfigExt | null => {
  14. const rendererConfig = useRendererConfig();
  15. const { isDarkMode } = useNextThemes();
  16. return rendererConfig == null
  17. ? null
  18. : ({
  19. ...rendererConfig,
  20. isDarkMode,
  21. } satisfies RendererConfigExt);
  22. };
  23. export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
  24. const currentPagePath = useCurrentPagePath();
  25. const rendererConfig = useRendererConfigExt();
  26. const setTocNode = useSetTocNode();
  27. // Store TOC node in a ref during render phase (called by rehype plugin inside ReactMarkdown),
  28. // then sync to atom after commit to avoid "Cannot update a component while rendering a different component"
  29. const pendingTocNodeRef = useRef<HtmlElementNode | null>(null);
  30. const storeTocNodeHandler = useCallback((toc: HtmlElementNode) => {
  31. pendingTocNodeRef.current = toc;
  32. }, []);
  33. // No dependency array: runs after every render because the ref mutation
  34. // is invisible to React's dependency tracking
  35. useEffect(() => {
  36. if (pendingTocNodeRef.current != null) {
  37. setTocNode(pendingTocNodeRef.current);
  38. pendingTocNodeRef.current = null;
  39. }
  40. });
  41. const isAllDataValid = currentPagePath != null && rendererConfig != null;
  42. const customGenerater =
  43. getGrowiFacade().markdownRenderer?.optionsGenerators
  44. ?.customGenerateViewOptions;
  45. return useSWR(
  46. isAllDataValid
  47. ? ['viewOptions', currentPagePath, rendererConfig, customGenerater]
  48. : null,
  49. async ([, currentPagePath, rendererConfig]) => {
  50. if (customGenerater != null) {
  51. return customGenerater(
  52. currentPagePath,
  53. rendererConfig,
  54. storeTocNodeHandler,
  55. );
  56. }
  57. const { generateViewOptions } = await import(
  58. '~/client/services/renderer/renderer'
  59. );
  60. return generateViewOptions(
  61. currentPagePath,
  62. rendererConfig,
  63. storeTocNodeHandler,
  64. );
  65. },
  66. {
  67. keepPreviousData: true,
  68. revalidateOnFocus: false,
  69. revalidateOnReconnect: false,
  70. },
  71. );
  72. };
  73. export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
  74. const currentPagePath = useCurrentPagePath();
  75. const rendererConfig = useRendererConfigExt();
  76. const isAllDataValid = currentPagePath != null && rendererConfig != null;
  77. const customGenerater =
  78. getGrowiFacade().markdownRenderer?.optionsGenerators
  79. ?.customGeneratePreviewOptions;
  80. return useSWR(
  81. isAllDataValid
  82. ? ['previewOptions', rendererConfig, currentPagePath, customGenerater]
  83. : null,
  84. async ([, rendererConfig, pagePath]) => {
  85. if (customGenerater != null) {
  86. return customGenerater(rendererConfig, pagePath);
  87. }
  88. const { generatePreviewOptions } = await import(
  89. '~/client/services/renderer/renderer'
  90. );
  91. return generatePreviewOptions(rendererConfig, pagePath);
  92. },
  93. {
  94. keepPreviousData: true,
  95. revalidateOnFocus: false,
  96. revalidateOnReconnect: false,
  97. },
  98. );
  99. };
  100. export const useCommentForCurrentPageOptions = (): SWRResponse<
  101. RendererOptions,
  102. Error
  103. > => {
  104. const currentPagePath = useCurrentPagePath();
  105. const rendererConfig = useRendererConfigExt();
  106. const isAllDataValid = currentPagePath != null && rendererConfig != null;
  107. return useSWR(
  108. isAllDataValid
  109. ? ['commentPreviewOptions', rendererConfig, currentPagePath]
  110. : null,
  111. async ([, rendererConfig, currentPagePath]) => {
  112. const { generateSimpleViewOptions } = await import(
  113. '~/client/services/renderer/renderer'
  114. );
  115. return generateSimpleViewOptions(
  116. rendererConfig,
  117. currentPagePath,
  118. undefined,
  119. rendererConfig.isEnabledLinebreaksInComments,
  120. );
  121. },
  122. {
  123. keepPreviousData: true,
  124. revalidateOnFocus: false,
  125. revalidateOnReconnect: false,
  126. },
  127. );
  128. };
  129. export const useCommentPreviewOptions = useCommentForCurrentPageOptions;
  130. export const useSelectedPagePreviewOptions = (
  131. pagePath: string,
  132. highlightKeywords?: string | string[],
  133. ): SWRResponse<RendererOptions, Error> => {
  134. const rendererConfig = useRendererConfigExt();
  135. const isAllDataValid = rendererConfig != null;
  136. return useSWR(
  137. isAllDataValid
  138. ? [
  139. 'selectedPagePreviewOptions',
  140. rendererConfig,
  141. pagePath,
  142. highlightKeywords,
  143. ]
  144. : null,
  145. async ([, rendererConfig, pagePath, highlightKeywords]) => {
  146. const { generateSimpleViewOptions } = await import(
  147. '~/client/services/renderer/renderer'
  148. );
  149. return generateSimpleViewOptions(
  150. rendererConfig,
  151. pagePath,
  152. highlightKeywords,
  153. );
  154. },
  155. {
  156. revalidateOnFocus: false,
  157. revalidateOnReconnect: false,
  158. },
  159. );
  160. };
  161. export const useSearchResultOptions = useSelectedPagePreviewOptions;
  162. export const useTimelineOptions = useSelectedPagePreviewOptions;
  163. export const useCustomSidebarOptions = (
  164. config?: SWRConfiguration,
  165. ): SWRResponse<RendererOptions, Error> => {
  166. const rendererConfig = useRendererConfigExt();
  167. const isAllDataValid = rendererConfig != null;
  168. return useSWR(
  169. isAllDataValid ? ['customSidebarOptions', rendererConfig] : null,
  170. async ([, rendererConfig]) => {
  171. const { generateSimpleViewOptions } = await import(
  172. '~/client/services/renderer/renderer'
  173. );
  174. return generateSimpleViewOptions(rendererConfig, '/');
  175. },
  176. {
  177. ...config,
  178. keepPreviousData: true,
  179. revalidateOnFocus: false,
  180. revalidateOnReconnect: false,
  181. },
  182. );
  183. };
  184. export const usePresentationViewOptions = (): SWRResponse<
  185. RendererOptions,
  186. Error
  187. > => {
  188. const currentPagePath = useCurrentPagePath();
  189. const rendererConfig = useRendererConfigExt();
  190. const isAllDataValid = currentPagePath != null && rendererConfig != null;
  191. useEffect(() => {
  192. if (rendererConfig == null) {
  193. logger.warn('RendererConfig is undefined or missing.');
  194. }
  195. }, [rendererConfig]);
  196. return useSWR(
  197. isAllDataValid
  198. ? ['presentationViewOptions', currentPagePath, rendererConfig]
  199. : null,
  200. async ([, currentPagePath, rendererConfig]) => {
  201. const { generatePresentationViewOptions } = await import(
  202. '~/client/services/renderer/renderer'
  203. );
  204. return generatePresentationViewOptions(rendererConfig, currentPagePath);
  205. },
  206. {
  207. revalidateOnFocus: false,
  208. revalidateOnReconnect: false,
  209. },
  210. );
  211. };