Skip to content

Commit

Permalink
feat: Add DocMeta component (#361)
Browse files Browse the repository at this point in the history
* feat: add dependencies

Add packages:
isomorphic-dompurify
hoist-non-react-statics
@types/hoist-non-react-statics
react-recompose
@types/react-recompose
react-helmet
@types/react-helmet

* feat: add wrapper for React Context

* feat: add context for language selection

* feat: add Query type

* feat: add context for router

* feat: add HOC for context

* feat: add HOC for language selection

* feat: add HOC for router

* feat: add HTML sanitizer

* feat: add DocMeta component
  • Loading branch information
Pavel-Tyan authored Jan 21, 2025
1 parent 5de8873 commit fc99858
Show file tree
Hide file tree
Showing 12 changed files with 738 additions and 17 deletions.
612 changes: 595 additions & 17 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,18 @@
"@gravity-ui/uikit": "^6.2.0",
"@popperjs/core": "^2.11.2",
"bem-cn-lite": "4.1.0",
"hoist-non-react-statics": "^3.3.2",
"i18next": "^19.9.2",
"isomorphic-dompurify": "^2.20.0",
"langs": "^2.0.0",
"lodash": "^4.17.21",
"mark.ts": "^1.0.5",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
"react-hotkeys-hook": "^3.3.1",
"react-i18next": "11.15.6",
"react-popper": "^2.2.5",
"react-recompose": "^0.33.0",
"react-transition-group": "^4.4.5",
"scroll-into-view-if-needed": "2.2.29",
"url": "^0.11.1"
Expand All @@ -98,12 +102,15 @@
"@diplodoc/lint": "^1.1.1",
"@diplodoc/tsconfig": "^1.0.2",
"@playwright/test": "^1.48.2",
"@types/hoist-non-react-statics": "^3.3.6",
"@types/langs": "^2.0.1",
"@types/lodash": "4.14.179",
"@types/node": "^22.9.0",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/react-gtm-module": "^2.0.3",
"@types/react-helmet": "^6.1.11",
"@types/react-recompose": "^0.33.6",
"@types/react-transition-group": "^4.4.10",
"autoprefixer": "^10.4.15",
"esbuild": "^0.23.1",
Expand Down
42 changes: 42 additions & 0 deletions src/components/DocMeta/DocMeta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import {Helmet} from 'react-helmet';
import {compose} from 'react-recompose';

import {sanitizeHtml} from '../../utils/sanitize';
import withRouter, {WithRouterProps} from '../../hoc/withRouter';

export interface DocMetaProps {
title?: string;
defaultTitle?: string;
titleTemplate?: string;
metadata?: Record<string, string>[];
}

type DocMetaInnerProps = DocMetaProps & WithRouterProps;

const sanitizeObject = (target: Record<string, string>) => {
const clear = Object.entries(target).map(
([name, content]) => [sanitizeHtml(name), sanitizeHtml(content)] as [string, string],
);

return Object.fromEntries(clear);
};

const DocMeta: React.FC<DocMetaInnerProps> = (props) => {
const title = sanitizeHtml(props.title);
const titleTemplate = sanitizeHtml(props.titleTemplate);
const defaultTitle = sanitizeHtml(props.defaultTitle);

const {metadata = []} = props;

return (
<Helmet title={title} defaultTitle={defaultTitle} titleTemplate={titleTemplate}>
{metadata.map((element, index) => {
/* list is immutable */
return <meta key={index} {...sanitizeObject(element)} />;
})}
</Helmet>
);
};

export default compose<DocMetaInnerProps, DocMetaProps>(withRouter)(DocMeta);
2 changes: 2 additions & 0 deletions src/components/DocMeta/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {default as DocMeta} from './DocMeta';
export * from './DocMeta';
8 changes: 8 additions & 0 deletions src/contexts/LangContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import namedContext from './_namedContext';

export enum Lang {
Ru = 'ru',
En = 'en',
}

export default namedContext<Lang>('Lang', (process.env.DEFAULT_LANG as Lang) || Lang.En);
15 changes: 15 additions & 0 deletions src/contexts/RouterContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type {Query} from '../models';

export interface RouterData {
page: string;
pathname: string;
query: Query;
hash?: string;
as: string;
hostname: string;
serverRouter?: RouterData;
}

import namedContext from './_namedContext';

export default namedContext<RouterData>('Request', {} as RouterData);
9 changes: 9 additions & 0 deletions src/contexts/_namedContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

export default function <T>(name: string, defaults: T): React.Context<T> {
const Context = React.createContext(defaults);

Context.displayName = name;

return Context;
}
30 changes: 30 additions & 0 deletions src/hoc/_withContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function <P extends Record<string, any>>(
prop: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Context: React.Context<any> & {name?: string},
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function <T extends Record<string, any>>(Component: React.ComponentType<any>) {
const contextName = Context.displayName || Context.name || 'Context';
const componentName = Component.displayName || Component.name || 'Component';

class WithContextProp extends React.Component<Omit<T, keyof P>> {
static displayName = `with${contextName}(${componentName})`;

static contextType = Context;

render() {
return <Component {...this.props} {...{[prop]: this.context}} />;
}
}

// Copies non-react specific statics from a child component to a parent component
hoistNonReactStatics(WithContextProp, Component);

return WithContextProp;
};
}
9 changes: 9 additions & 0 deletions src/hoc/withLang.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import LangContext, {Lang} from '../contexts/LangContext';

import withPropContext from './_withContext';

export interface WithLangProps {
lang: Lang;
}

export default withPropContext<WithLangProps>('lang', LangContext);
9 changes: 9 additions & 0 deletions src/hoc/withRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import RouterContext, {RouterData} from '../contexts/RouterContext';

import withPropContext from './_withContext';

export interface WithRouterProps {
router: RouterData;
}

export default withPropContext<WithRouterProps>('router', RouterContext);
2 changes: 2 additions & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,5 @@ export interface ISearchResult {
description?: string;
breadcrumbs?: BreadcrumbItem[];
}

export type Query = Record<string, number | string | null>;
10 changes: 10 additions & 0 deletions src/utils/sanitize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {sanitize} from 'isomorphic-dompurify';

const sanitizeStripOptions = {
ALLOWED_TAGS: [],
ALLOWED_ATTR: [],
};

export function sanitizeHtml(html?: string) {
return html && sanitize(html, sanitizeStripOptions);
}

0 comments on commit fc99858

Please sign in to comment.