Browse Source

Merge pull request #6439 from weseek/feat/rehype-relative-links

feat: Rehype relative links
Yuki Takei 3 years ago
parent
commit
8a37fb505c

+ 1 - 0
packages/app/next.config.js

@@ -29,6 +29,7 @@ const setupTranspileModules = () => {
     'decode-named-character-reference',
     'hastscript',
     'html-void-elements',
+    'is-absolute-url',
     'longest-streak',
     'property-information',
     'space-separated-tokens',

+ 1 - 0
packages/app/package.json

@@ -115,6 +115,7 @@
     "i18next-chained-backend": "^3.0.2",
     "i18next-http-backend": "^1.4.1",
     "i18next-localstorage-backend": "^3.1.3",
+    "is-absolute-url": "^4.0.1",
     "is-iso-date": "^0.0.1",
     "lucene-query-parser": "^1.2.0",
     "md5": "^2.2.1",

+ 1 - 1
packages/app/src/components/Page.jsx

@@ -204,7 +204,7 @@ export const Page = (props) => {
 
     blinkSectionHeaderAtBoot();
     mutateBlinkedAtBoot(true);
-  }, [mutateBlinkedAtBoot]);
+  }, [isBlinkedAtBoot, mutateBlinkedAtBoot]);
 
   // // set handler to open DrawioModal
   // useEffect(() => {

+ 0 - 4
packages/app/src/interfaces/services/renderer.ts

@@ -1,5 +1,3 @@
-import { HastNode } from 'hast-util-select';
-
 import { XssOptionConfig } from '~/services/xss/xssOption';
 
 // export type GrowiHydratedEnv = {
@@ -20,5 +18,3 @@ export type RendererConfig = {
   plantumlUri: string | null,
   blockdiagUri: string | null,
 } & XssOptionConfig;
-
-export type RehypePlugin = (option: any) => (node: HastNode) => void

+ 3 - 5
packages/app/src/services/renderer/rehype-plugins/add-class.ts

@@ -1,9 +1,7 @@
 // See: https://github.com/martypdx/rehype-add-classes for the original implementation.
 // Re-implemeted in TypeScript.
-
 import { selectAll, HastNode, Element } from 'hast-util-select';
-
-import { RehypePlugin } from '~/interfaces/services/renderer';
+import { Plugin } from 'unified';
 
 export type SelectorName = string; // e.g. 'h1'
 export type ClassName = string; // e.g. 'header'
@@ -32,8 +30,8 @@ const adder = (entry: AdditionsEntry) => {
   return (node: HastNode) => selectAll(selectorName, node).forEach(writer);
 };
 
-export const addClass: RehypePlugin = (additions) => {
+export const addClass: Plugin<[Additions]> = (additions) => {
   const adders = Object.entries(additions).map(adder);
 
-  return node => adders.forEach(a => a(node));
+  return node => adders.forEach(a => a(node as HastNode));
 };

+ 35 - 0
packages/app/src/services/renderer/rehype-plugins/relative-links.ts

@@ -0,0 +1,35 @@
+import { selectAll, HastNode } from 'hast-util-select';
+import isAbsolute from 'is-absolute-url';
+import { Plugin } from 'unified';
+
+type RelativeLinksPluginParams = {
+  pagePath?: string,
+}
+
+export const relativeLinks: Plugin<[RelativeLinksPluginParams]> = (options = {}) => {
+  return (tree) => {
+    if (options.pagePath == null) {
+      return;
+    }
+
+    const pagePath = options.pagePath;
+    const anchors = selectAll('a[href]', tree as HastNode);
+
+    anchors.forEach((anchor) => {
+      if (anchor.properties == null) {
+        return;
+      }
+
+      const href = anchor.properties.href;
+      if (href == null || typeof href !== 'string' || isAbsolute(href)) {
+        return;
+      }
+
+      // generate relative pathname
+      const baseUrl = new URL(pagePath, 'https://example.com');
+      const relativeUrl = new URL(href, baseUrl);
+
+      anchor.properties.href = relativeUrl.pathname;
+    });
+  };
+};

+ 14 - 13
packages/app/src/services/renderer/renderer.ts

@@ -13,9 +13,11 @@ import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import { RendererConfig } from '~/interfaces/services/renderer';
-import { addClass } from '~/services/renderer/rehype-plugins/add-class';
 import loggerFactory from '~/utils/logger';
 
+import { addClass } from './rehype-plugins/add-class';
+import { relativeLinks } from './rehype-plugins/relative-links';
+
 // import CsvToTable from './PreProcessor/CsvToTable';
 // import EasyGrid from './PreProcessor/EasyGrid';
 // import Linker from './PreProcessor/Linker';
@@ -212,15 +214,13 @@ const logger = loggerFactory('growi:util:GrowiRenderer');
 
 export type RendererOptions = Partial<ReactMarkdownOptions>;
 
-export interface ReactMarkdownOptionsGenerator {
-  (config: RendererConfig): RendererOptions
-}
 
-const generateCommonOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
+const generateCommonOptions = (pagePath: string|undefined, config: RendererConfig): RendererOptions => {
   return {
     remarkPlugins: [gfm],
     rehypePlugins: [
       slug,
+      [relativeLinks, { pagePath }],
       raw,
       [sanitize, {
         ...sanitizeDefaultSchema,
@@ -243,11 +243,12 @@ const generateCommonOptions: ReactMarkdownOptionsGenerator = (config: RendererCo
 };
 
 export const generateViewOptions = (
+    pagePath: string,
     config: RendererConfig,
     storeTocNode: (node: HtmlElementNode) => void,
 ): RendererOptions => {
 
-  const options = generateCommonOptions(config);
+  const options = generateCommonOptions(pagePath, config);
 
   const { remarkPlugins, rehypePlugins, components } = options;
 
@@ -312,7 +313,7 @@ export const generateViewOptions = (
 
 export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementNode | undefined): RendererOptions => {
 
-  const options = generateCommonOptions(config);
+  const options = generateCommonOptions(undefined, config);
 
   const { remarkPlugins, rehypePlugins } = options;
 
@@ -334,8 +335,8 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
   return options;
 };
 
-export const generatePreviewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
-  const options = generateCommonOptions(config);
+export const generatePreviewOptions = (config: RendererConfig): RendererOptions => {
+  const options = generateCommonOptions(undefined, config);
 
   // // Add configurers for preview
   // renderer.addConfigurers([
@@ -350,8 +351,8 @@ export const generatePreviewOptions: ReactMarkdownOptionsGenerator = (config: Re
   return options;
 };
 
-export const generateCommentPreviewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
-  const options = generateCommonOptions(config);
+export const generateCommentPreviewOptions = (config: RendererConfig): RendererOptions => {
+  const options = generateCommonOptions(undefined, config);
   const { remarkPlugins } = options;
 
   // add remark plugins
@@ -372,8 +373,8 @@ export const generateCommentPreviewOptions: ReactMarkdownOptionsGenerator = (con
   return options;
 };
 
-export const generateOthersOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
-  const options = generateCommonOptions(config);
+export const generateOthersOptions = (config: RendererConfig): RendererOptions => {
+  const options = generateCommonOptions(undefined, config);
 
   // renderer.addConfigurers([
   //   new TableConfigurer(),

+ 19 - 5
packages/app/src/stores/renderer.tsx

@@ -1,14 +1,19 @@
 import { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
+import { RendererConfig } from '~/interfaces/services/renderer';
 import {
-  ReactMarkdownOptionsGenerator, RendererOptions,
+  RendererOptions,
   generatePreviewOptions, generateCommentPreviewOptions, generateOthersOptions,
   generateViewOptions, generateTocOptions,
 } from '~/services/renderer/renderer';
 
 
-import { useCurrentPageTocNode, useRendererConfig } from './context';
+import { useCurrentPagePath, useCurrentPageTocNode, useRendererConfig } from './context';
+
+interface ReactMarkdownOptionsGenerator {
+  (config: RendererConfig): RendererOptions
+}
 
 // The base hook with common processes
 const _useOptionsBase = (
@@ -33,11 +38,20 @@ const _useOptionsBase = (
 };
 
 export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
-  const key = 'viewOptions';
-
+  const { data: currentPagePath } = useCurrentPagePath();
+  const { data: rendererConfig } = useRendererConfig();
   const { mutate: storeTocNode } = useCurrentPageTocNode();
 
-  return _useOptionsBase(key, config => generateViewOptions(config, storeTocNode));
+  const isAllDataValid = currentPagePath != null && rendererConfig != null;
+
+  const key = isAllDataValid
+    ? ['viewOptions', currentPagePath, rendererConfig]
+    : null;
+
+  return useSWRImmutable<RendererOptions, Error>(
+    key,
+    (rendererId, currentPagePath, rendererConfig) => generateViewOptions(currentPagePath, rendererConfig, storeTocNode),
+  );
 };
 
 export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {

+ 5 - 0
yarn.lock

@@ -11606,6 +11606,11 @@ ipaddr.js@1.9.1:
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
   integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
 
+is-absolute-url@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-4.0.1.tgz#16e4d487d4fded05cfe0685e53ec86804a5e94dc"
+  integrity sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==
+
 is-accessor-descriptor@^0.1.6:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"