Reimplement preview styles, add frame flag, clean up markdown editor options
This commit is contained in:
@ -174,6 +174,10 @@ export function applyDefaults(originalConfig: Config) {
|
||||
for (const collection of config.collections) {
|
||||
let collectionI18n = collection[I18N];
|
||||
|
||||
if (config.editor && !collection.editor) {
|
||||
collection.editor = { preview: config.editor.preview, frame: config.editor.frame };
|
||||
}
|
||||
|
||||
if (i18n && collectionI18n) {
|
||||
collectionI18n = getI18nDefaults(collectionI18n, i18n);
|
||||
collection[I18N] = collectionI18n;
|
||||
@ -240,6 +244,10 @@ export function applyDefaults(originalConfig: Config) {
|
||||
if (file.fields) {
|
||||
file.fields = setI18nDefaultsForFields(file.fields, Boolean(fileI18n));
|
||||
}
|
||||
|
||||
if (collection.editor && !file.editor) {
|
||||
file.editor = { preview: collection.editor.preview, frame: collection.editor.frame };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,10 +274,6 @@ export function applyDefaults(originalConfig: Config) {
|
||||
id: `${group.field}__${group.pattern}`,
|
||||
};
|
||||
});
|
||||
|
||||
if (config.editor && !collection.editor) {
|
||||
collection.editor = { preview: config.editor.preview };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -293,4 +293,4 @@ const mapDispatchToProps = {
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
export type CollectionViewProps = ConnectedProps<typeof connector>;
|
||||
|
||||
export default translate()(connector(CollectionView)) as ComponentType<CollectionViewOwnProps>;
|
||||
export default connector(translate()(CollectionView) as ComponentType<CollectionViewProps>);
|
||||
|
@ -146,12 +146,10 @@ const EditorControl = ({
|
||||
submitted,
|
||||
getAsset,
|
||||
isDisabled,
|
||||
isEditorComponent,
|
||||
isFetching,
|
||||
isFieldDuplicate,
|
||||
isFieldHidden,
|
||||
isHidden = false,
|
||||
isNewEditorComponent,
|
||||
loadEntry,
|
||||
locale,
|
||||
mediaPaths,
|
||||
@ -223,11 +221,9 @@ const EditorControl = ({
|
||||
submitted,
|
||||
getAsset: handleGetAsset(collection, entry),
|
||||
isDisabled: isDisabled ?? false,
|
||||
isEditorComponent: isEditorComponent ?? false,
|
||||
isFetching,
|
||||
isFieldDuplicate,
|
||||
isFieldHidden,
|
||||
isNewEditorComponent: isNewEditorComponent ?? false,
|
||||
label: getFieldLabel(field, t),
|
||||
loadEntry,
|
||||
locale,
|
||||
@ -270,11 +266,9 @@ interface EditorControlOwnProps {
|
||||
fieldsErrors: FieldsErrors;
|
||||
submitted: boolean;
|
||||
isDisabled?: boolean;
|
||||
isEditorComponent?: boolean;
|
||||
isFieldDuplicate?: (field: Field) => boolean;
|
||||
isFieldHidden?: (field: Field) => boolean;
|
||||
isHidden?: boolean;
|
||||
isNewEditorComponent?: boolean;
|
||||
locale?: string;
|
||||
parentPath: string;
|
||||
value: ValueOrNestedValue;
|
||||
|
@ -122,17 +122,6 @@ const EditorContent = ({
|
||||
}
|
||||
};
|
||||
|
||||
function isPreviewEnabled(collection: Collection, entry: Entry) {
|
||||
if (collection.type === FILES) {
|
||||
const file = getFileFromSlug(collection, entry.slug);
|
||||
const previewEnabled = file?.editor?.preview ?? false;
|
||||
if (previewEnabled) {
|
||||
return previewEnabled;
|
||||
}
|
||||
}
|
||||
return collection.editor?.preview ?? true;
|
||||
}
|
||||
|
||||
interface EditorInterfaceProps {
|
||||
draftKey: string;
|
||||
entry: Entry;
|
||||
@ -228,7 +217,23 @@ const EditorInterface = ({
|
||||
setSelectedLocale(locale);
|
||||
}, []);
|
||||
|
||||
const previewEnabled = isPreviewEnabled(collection, entry);
|
||||
const [previewEnabled, previewInFrame] = useMemo(() => {
|
||||
let preview = collection.editor?.preview ?? true;
|
||||
let frame = collection.editor?.frame ?? true;
|
||||
|
||||
if (collection.type === FILES) {
|
||||
const file = getFileFromSlug(collection, entry.slug);
|
||||
if (file?.editor?.preview !== undefined) {
|
||||
preview = file.editor.preview;
|
||||
}
|
||||
|
||||
if (file?.editor?.frame !== undefined) {
|
||||
frame = file.editor.frame;
|
||||
}
|
||||
}
|
||||
|
||||
return [preview, frame];
|
||||
}, [collection, entry.slug]);
|
||||
|
||||
const collectionI18nEnabled = hasI18n(collection);
|
||||
|
||||
@ -271,7 +276,12 @@ const EditorInterface = ({
|
||||
<StyledSplitPane>
|
||||
<ScrollSyncPane>{editor}</ScrollSyncPane>
|
||||
<PreviewPaneContainer>
|
||||
<EditorPreviewPane collection={collection} entry={previewEntry} fields={fields} />
|
||||
<EditorPreviewPane
|
||||
collection={collection}
|
||||
previewInFrame={previewInFrame}
|
||||
entry={previewEntry}
|
||||
fields={fields}
|
||||
/>
|
||||
</PreviewPaneContainer>
|
||||
</StyledSplitPane>
|
||||
</>
|
||||
|
@ -1,34 +1,15 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { ScrollSyncPane } from 'react-scroll-sync';
|
||||
|
||||
import type { TemplatePreviewComponent, TemplatePreviewProps } from '../../../interface';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { TemplatePreviewComponent, TemplatePreviewProps } from '../../../interface';
|
||||
|
||||
interface PreviewContentProps {
|
||||
previewComponent?: TemplatePreviewComponent;
|
||||
previewProps: TemplatePreviewProps;
|
||||
}
|
||||
|
||||
const StyledPreviewContent = styled('div')`
|
||||
width: calc(100% - min(864px, 50%));
|
||||
top: 64px;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
const PreviewContent = ({ previewComponent, previewProps }: PreviewContentProps) => {
|
||||
const element = useMemo(() => document.getElementById('cms-root'), []);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let children: ReactNode;
|
||||
if (!previewComponent) {
|
||||
children = null;
|
||||
@ -38,14 +19,8 @@ const PreviewContent = ({ previewComponent, previewProps }: PreviewContentProps)
|
||||
children = React.createElement(previewComponent, previewProps);
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<ScrollSyncPane>
|
||||
<StyledPreviewContent className="preview-content">{children}</StyledPreviewContent>
|
||||
</ScrollSyncPane>,
|
||||
element,
|
||||
'preview-content',
|
||||
);
|
||||
}, [previewComponent, previewProps, element]);
|
||||
return children;
|
||||
}, [previewComponent, previewProps]);
|
||||
};
|
||||
|
||||
export default PreviewContent;
|
||||
|
@ -1,11 +1,15 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import React, { isValidElement, useCallback, useMemo } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Frame, { FrameContextConsumer } from 'react-frame-component';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { connect } from 'react-redux';
|
||||
import { ScrollSyncPane } from 'react-scroll-sync';
|
||||
|
||||
import { getAsset as getAssetAction } from '../../../actions/media';
|
||||
import { lengths } from '../../../components/UI/styles';
|
||||
import { INFERABLE_FIELDS } from '../../../constants/fieldInference';
|
||||
import { getPreviewTemplate, getRemarkPlugins, resolveWidget } from '../../../lib/registry';
|
||||
import { getPreviewStyles, getPreviewTemplate, resolveWidget } from '../../../lib/registry';
|
||||
import { selectInferedField, selectTemplateName } from '../../../lib/util/collection.util';
|
||||
import { selectField } from '../../../lib/util/field.util';
|
||||
import { selectIsLoadingAsset } from '../../../reducers/medias';
|
||||
@ -14,21 +18,22 @@ import EditorPreview from './EditorPreview';
|
||||
import EditorPreviewContent from './EditorPreviewContent';
|
||||
import PreviewHOC from './PreviewHOC';
|
||||
|
||||
import type { ReactFragment, ReactNode } from 'react';
|
||||
import type { ComponentType, ReactFragment, ReactNode } from 'react';
|
||||
import type { ConnectedProps } from 'react-redux';
|
||||
import type { InferredField } from '../../../constants/fieldInference';
|
||||
import type {
|
||||
Field,
|
||||
TemplatePreviewProps,
|
||||
Collection,
|
||||
Entry,
|
||||
EntryData,
|
||||
Field,
|
||||
GetAssetFunction,
|
||||
TemplatePreviewProps,
|
||||
TranslatedProps,
|
||||
ValueOrNestedValue,
|
||||
} from '../../../interface';
|
||||
import type { RootState } from '../../../store';
|
||||
|
||||
const PreviewPaneFrame = styled('div')`
|
||||
const PreviewPaneFrame = styled(Frame)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
@ -37,6 +42,25 @@ const PreviewPaneFrame = styled('div')`
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const PreviewPaneWrapper = styled('div')`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: #fff;
|
||||
border-radius: ${lengths.borderRadius};
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const StyledPreviewContent = styled('div')`
|
||||
width: calc(100% - min(864px, 50%));
|
||||
top: 64px;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Returns the widget component for a named field, and makes recursive calls
|
||||
* to retrieve components for nested and deeply nested fields, which occur in
|
||||
@ -162,7 +186,6 @@ function getWidget(
|
||||
}
|
||||
entry={entry}
|
||||
resolveWidget={resolveWidget}
|
||||
getRemarkPlugins={getRemarkPlugins}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -234,8 +257,8 @@ function getNestedWidgets(
|
||||
);
|
||||
}
|
||||
|
||||
const PreviewPane = (props: EditorPreviewPaneProps) => {
|
||||
const { entry, collection, config, fields, getAsset } = props;
|
||||
const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
|
||||
const { entry, collection, config, fields, previewInFrame, getAsset, t } = props;
|
||||
|
||||
const inferedFields = useMemo(() => {
|
||||
const titleField = selectInferedField(collection, 'title');
|
||||
@ -307,39 +330,120 @@ const PreviewPane = (props: EditorPreviewPaneProps) => {
|
||||
[collection, entry, fields, handleGetAsset, inferedFields],
|
||||
);
|
||||
|
||||
if (!entry || !entry.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previewComponent =
|
||||
getPreviewTemplate(selectTemplateName(collection, entry.slug)) ?? EditorPreview;
|
||||
|
||||
const previewProps: TemplatePreviewProps = {
|
||||
...props,
|
||||
getAsset: handleGetAsset,
|
||||
widgetFor,
|
||||
widgetsFor,
|
||||
};
|
||||
|
||||
if (!collection) {
|
||||
<PreviewPaneFrame id="preview-pane" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary config={config}>
|
||||
<PreviewPaneFrame id="preview-pane">
|
||||
<EditorPreviewContent
|
||||
{...{ previewComponent, previewProps: { ...previewProps, document, window } }}
|
||||
/>
|
||||
</PreviewPaneFrame>
|
||||
</ErrorBoundary>
|
||||
const previewStyles = useMemo(
|
||||
() =>
|
||||
getPreviewStyles().map((style, i) => {
|
||||
if (style.raw) {
|
||||
return <style key={i}>{style.value}</style>;
|
||||
}
|
||||
return <link key={i} href={style.value} type="text/css" rel="stylesheet" />;
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const previewComponent = useMemo(
|
||||
() => getPreviewTemplate(selectTemplateName(collection, entry.slug)) ?? EditorPreview,
|
||||
[collection, entry.slug],
|
||||
);
|
||||
|
||||
const initialFrameContent = useMemo(
|
||||
() => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><base target="_blank"/></head>
|
||||
<body><div></div></body>
|
||||
</html>
|
||||
`,
|
||||
[],
|
||||
);
|
||||
|
||||
const element = useMemo(() => document.getElementById('cms-root'), []);
|
||||
|
||||
const previewProps: Omit<TemplatePreviewProps, 'document' | 'window'> = useMemo(
|
||||
() => ({
|
||||
...props,
|
||||
getAsset: handleGetAsset,
|
||||
widgetFor,
|
||||
widgetsFor,
|
||||
}),
|
||||
[handleGetAsset, props, widgetFor, widgetsFor],
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<ScrollSyncPane>
|
||||
<StyledPreviewContent className="preview-content">
|
||||
{!entry || !entry.data ? null : (
|
||||
<ErrorBoundary config={config}>
|
||||
{previewInFrame ? (
|
||||
<PreviewPaneFrame
|
||||
key="preview-frame"
|
||||
id="preview-pane"
|
||||
head={previewStyles}
|
||||
initialContent={initialFrameContent}
|
||||
>
|
||||
{!collection ? (
|
||||
t('collection.notFound')
|
||||
) : (
|
||||
<FrameContextConsumer>
|
||||
{({ document, window }) => {
|
||||
return (
|
||||
<EditorPreviewContent
|
||||
key="preview-frame-content"
|
||||
previewComponent={previewComponent}
|
||||
previewProps={{ ...previewProps, document, window }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</FrameContextConsumer>
|
||||
)}
|
||||
</PreviewPaneFrame>
|
||||
) : (
|
||||
<PreviewPaneWrapper key="preview-wrapper" id="preview-pane">
|
||||
{!collection ? (
|
||||
t('collection.notFound')
|
||||
) : (
|
||||
<>
|
||||
{previewStyles}
|
||||
<EditorPreviewContent
|
||||
key="preview-wrapper-content"
|
||||
previewComponent={previewComponent}
|
||||
previewProps={{ ...previewProps, document, window }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PreviewPaneWrapper>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</StyledPreviewContent>
|
||||
</ScrollSyncPane>,
|
||||
element,
|
||||
'preview-content',
|
||||
);
|
||||
}, [
|
||||
collection,
|
||||
config,
|
||||
element,
|
||||
entry,
|
||||
initialFrameContent,
|
||||
previewComponent,
|
||||
previewInFrame,
|
||||
previewProps,
|
||||
previewStyles,
|
||||
t,
|
||||
]);
|
||||
};
|
||||
|
||||
export interface EditorPreviewPaneOwnProps {
|
||||
collection: Collection;
|
||||
fields: Field[];
|
||||
entry: Entry;
|
||||
previewInFrame: boolean;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: RootState, ownProps: EditorPreviewPaneOwnProps) {
|
||||
@ -354,4 +458,4 @@ const mapDispatchToProps = {
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
export type EditorPreviewPaneProps = ConnectedProps<typeof connector>;
|
||||
|
||||
export default connector(PreviewPane);
|
||||
export default connector(translate()(PreviewPane) as ComponentType<EditorPreviewPaneProps>);
|
||||
|
@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { EditorComponentManualOptions } from '../../interface';
|
||||
|
||||
interface ImageData {
|
||||
alt: string;
|
||||
image: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const image: EditorComponentManualOptions<ImageData> = {
|
||||
label: 'Image',
|
||||
id: 'image',
|
||||
fromBlock: match =>
|
||||
match && {
|
||||
image: match[2],
|
||||
alt: match[1],
|
||||
title: match[4],
|
||||
},
|
||||
toBlock: ({ alt, image, title }) =>
|
||||
`}"` : ''})`,
|
||||
// eslint-disable-next-line react/display-name
|
||||
toPreview: ({ alt, image, title }, getAsset, fields) => {
|
||||
const imageField = fields?.find(f => f.widget === 'image');
|
||||
const src = getAsset(image, imageField).toString();
|
||||
return <img src={src || ''} alt={alt || ''} title={title || ''} />;
|
||||
},
|
||||
pattern: /^!\[(.*)\]\((.*?)(\s"(.*)")?\)$/,
|
||||
fields: [
|
||||
{
|
||||
label: 'Image',
|
||||
name: 'image',
|
||||
widget: 'image',
|
||||
media_library: {
|
||||
allow_multiple: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Alt Text',
|
||||
name: 'alt',
|
||||
},
|
||||
{
|
||||
label: 'Title',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const StaticCmsEditorComponentImage = image;
|
||||
export default image;
|
@ -1 +0,0 @@
|
||||
export { default as imageEditorComponent } from './image';
|
@ -7,10 +7,8 @@ import {
|
||||
ProxyBackend,
|
||||
TestBackend,
|
||||
} from './backends';
|
||||
import { imageEditorComponent } from './editor-components';
|
||||
import {
|
||||
registerBackend,
|
||||
registerEditorComponent,
|
||||
registerLocale,
|
||||
registerWidget,
|
||||
} from './lib/registry';
|
||||
@ -59,12 +57,5 @@ export function addExtensions() {
|
||||
CodeWidget(),
|
||||
ColorStringWidget(),
|
||||
]);
|
||||
registerEditorComponent(imageEditorComponent);
|
||||
registerEditorComponent({
|
||||
id: 'code-block',
|
||||
label: 'Code Block',
|
||||
widget: 'code',
|
||||
type: 'code-block',
|
||||
});
|
||||
registerLocale('en', locales.en);
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import Registry from './lib/registry';
|
||||
export * from './backends';
|
||||
export * from './widgets';
|
||||
export * from './media-libraries';
|
||||
export * from './editor-components';
|
||||
export * from './locales';
|
||||
export * from './lib';
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import type { EditorPlugin, EditorType, WidgetRule } from '@toast-ui/editor/types/editor';
|
||||
import type { ToolbarItemOptions } from '@toast-ui/editor/types/ui';
|
||||
import type { PropertiesSchema } from 'ajv/dist/types/json-schema';
|
||||
import type { ComponentType, ReactNode } from 'react';
|
||||
import type { t, TranslateProps as ReactPolyglotTranslateProps } from 'react-polyglot';
|
||||
import type { Pluggable } from 'unified';
|
||||
import type { MediaFile as BackendMediaFile } from './backend';
|
||||
import type { EditorControlProps } from './components/Editor/EditorControlPane/EditorControl';
|
||||
import type { formatExtensions } from './formats/formats';
|
||||
@ -126,6 +127,11 @@ export interface FilterRule {
|
||||
field: string;
|
||||
}
|
||||
|
||||
export interface EditorConfig {
|
||||
preview?: boolean;
|
||||
frame?: boolean;
|
||||
}
|
||||
|
||||
export interface CollectionFile {
|
||||
name: string;
|
||||
label: string;
|
||||
@ -136,9 +142,7 @@ export interface CollectionFile {
|
||||
media_folder?: string;
|
||||
public_folder?: string;
|
||||
i18n?: boolean | I18nInfo;
|
||||
editor?: {
|
||||
preview?: boolean;
|
||||
};
|
||||
editor?: EditorConfig;
|
||||
}
|
||||
|
||||
interface Nested {
|
||||
@ -189,9 +193,7 @@ export interface Collection {
|
||||
nested?: Nested;
|
||||
i18n?: boolean | I18nInfo;
|
||||
hide?: boolean;
|
||||
editor?: {
|
||||
preview?: boolean;
|
||||
};
|
||||
editor?: EditorConfig;
|
||||
}
|
||||
|
||||
export type Collections = Record<string, Collection>;
|
||||
@ -236,11 +238,9 @@ export interface WidgetControlProps<T, F extends Field = Field> {
|
||||
forList: boolean;
|
||||
getAsset: GetAssetFunction;
|
||||
isDisabled: boolean;
|
||||
isEditorComponent: boolean;
|
||||
isFetching: boolean;
|
||||
isFieldDuplicate: EditorControlProps['isFieldDuplicate'];
|
||||
isFieldHidden: EditorControlProps['isFieldHidden'];
|
||||
isNewEditorComponent: boolean;
|
||||
label: string;
|
||||
loadEntry: EditorControlProps['loadEntry'];
|
||||
locale: string | undefined;
|
||||
@ -262,7 +262,6 @@ export interface WidgetPreviewProps<T = unknown, F extends Field = Field> {
|
||||
entry: Entry;
|
||||
field: F;
|
||||
getAsset: GetAssetFunction;
|
||||
getRemarkPlugins: () => Pluggable[];
|
||||
resolveWidget: <W = unknown, WF extends Field = Field>(name: string) => Widget<W, WF>;
|
||||
value: T | undefined | null;
|
||||
}
|
||||
@ -276,6 +275,8 @@ export interface TemplatePreviewProps {
|
||||
collection: Collection;
|
||||
fields: Field[];
|
||||
entry: Entry;
|
||||
document: Document | undefined | null;
|
||||
window: Window | undefined | null;
|
||||
getAsset: GetAssetFunction;
|
||||
widgetFor: (name: string) => ReactNode;
|
||||
widgetsFor: (name: string) =>
|
||||
@ -780,9 +781,7 @@ export interface Config {
|
||||
slug?: Slug;
|
||||
i18n?: I18nInfo;
|
||||
local_backend?: boolean | LocalBackend;
|
||||
editor?: {
|
||||
preview?: boolean;
|
||||
};
|
||||
editor?: EditorConfig;
|
||||
search?: boolean;
|
||||
}
|
||||
|
||||
@ -798,34 +797,6 @@ export interface BackendInitializer {
|
||||
init: (config: Config, options: BackendInitializerOptions) => BackendClass;
|
||||
}
|
||||
|
||||
export interface EditorComponentWidgetOptions {
|
||||
id: string;
|
||||
label: string;
|
||||
widget?: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface EditorComponentManualOptions<T = EntryData> {
|
||||
id: string;
|
||||
label: string;
|
||||
fields: Field[];
|
||||
pattern: RegExp;
|
||||
allow_add?: boolean;
|
||||
fromBlock: (match: RegExpMatchArray) => T;
|
||||
toBlock: (data: T) => string;
|
||||
toPreview: (data: T, getAsset: GetAssetFunction, fields: Field[]) => ReactNode;
|
||||
}
|
||||
|
||||
export function isEditorComponentWidgetOptions(
|
||||
options: EditorComponentOptions,
|
||||
): options is EditorComponentWidgetOptions {
|
||||
return 'widget' in options;
|
||||
}
|
||||
|
||||
export type EditorComponentOptions<T = EntryData> =
|
||||
| EditorComponentManualOptions<T>
|
||||
| EditorComponentWidgetOptions;
|
||||
|
||||
export interface EventData {
|
||||
entry: Entry;
|
||||
author: { login: string | undefined; name: string };
|
||||
@ -918,7 +889,39 @@ export interface ProcessedCodeLanguage {
|
||||
codemirror_mime_type: string;
|
||||
}
|
||||
|
||||
export type FileMetadata = {
|
||||
export interface FileMetadata {
|
||||
author: string;
|
||||
updatedOn: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PreviewStyleOptions {
|
||||
raw?: boolean;
|
||||
}
|
||||
|
||||
export interface PreviewStyle {
|
||||
value: string;
|
||||
raw: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetRulesFactoryProps {
|
||||
getAsset: GetAssetFunction;
|
||||
field: MarkdownField;
|
||||
}
|
||||
|
||||
export type WidgetRulesFactory = (props: WidgetRulesFactoryProps) => WidgetRule[];
|
||||
|
||||
export interface ToolbarItemsFactoryProps {
|
||||
imageToolbarButton: ToolbarItemOptions;
|
||||
}
|
||||
|
||||
export type ToolbarItemsFactory = (
|
||||
props: ToolbarItemsFactoryProps,
|
||||
) => (string | ToolbarItemOptions)[][];
|
||||
|
||||
export interface MarkdownEditorOptions {
|
||||
widgetRules?: WidgetRulesFactory;
|
||||
initialEditType?: EditorType;
|
||||
height?: string;
|
||||
toolbarItems?: ToolbarItemsFactory;
|
||||
plugins?: EditorPlugin[];
|
||||
}
|
||||
|
@ -1,28 +1,27 @@
|
||||
import { oneLine } from 'common-tags';
|
||||
|
||||
import EditorComponent from '../valueObjects/EditorComponent';
|
||||
|
||||
import type { Pluggable } from 'unified';
|
||||
import type {
|
||||
AdditionalLink,
|
||||
BackendClass,
|
||||
BackendInitializer,
|
||||
BackendInitializerOptions,
|
||||
Config,
|
||||
EventListener,
|
||||
Field,
|
||||
CustomIcon,
|
||||
LocalePhrasesRoot,
|
||||
MediaLibraryExternalLibrary,
|
||||
MediaLibraryOptions,
|
||||
TemplatePreviewComponent,
|
||||
WidgetParam,
|
||||
WidgetValueSerializer,
|
||||
EditorComponentOptions,
|
||||
Entry,
|
||||
EventData,
|
||||
EventListener,
|
||||
Field,
|
||||
LocalePhrasesRoot,
|
||||
MarkdownEditorOptions,
|
||||
MediaLibraryExternalLibrary,
|
||||
MediaLibraryOptions,
|
||||
PreviewStyle,
|
||||
PreviewStyleOptions,
|
||||
TemplatePreviewComponent,
|
||||
Widget,
|
||||
WidgetOptions,
|
||||
WidgetParam,
|
||||
WidgetValueSerializer,
|
||||
} from '../interface';
|
||||
|
||||
export const allowedEvents = ['prePublish', 'postPublish', 'preSave', 'postSave'] as const;
|
||||
@ -39,12 +38,14 @@ interface Registry {
|
||||
widgets: Record<string, Widget>;
|
||||
icons: Record<string, CustomIcon>;
|
||||
additionalLinks: Record<string, AdditionalLink>;
|
||||
editorComponents: Record<string, EditorComponentOptions>;
|
||||
remarkPlugins: Pluggable[];
|
||||
widgetValueSerializers: Record<string, WidgetValueSerializer>;
|
||||
mediaLibraries: (MediaLibraryExternalLibrary & { options: MediaLibraryOptions })[];
|
||||
locales: Record<string, LocalePhrasesRoot>;
|
||||
eventHandlers: typeof eventHandlers;
|
||||
previewStyles: PreviewStyle[];
|
||||
|
||||
/** Markdown editor */
|
||||
markdownEditorConfig: MarkdownEditorOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,12 +57,12 @@ const registry: Registry = {
|
||||
widgets: {},
|
||||
icons: {},
|
||||
additionalLinks: {},
|
||||
editorComponents: {},
|
||||
remarkPlugins: [],
|
||||
widgetValueSerializers: {},
|
||||
mediaLibraries: [],
|
||||
locales: {},
|
||||
eventHandlers,
|
||||
previewStyles: [],
|
||||
markdownEditorConfig: {},
|
||||
};
|
||||
|
||||
export default {
|
||||
@ -71,10 +72,6 @@ export default {
|
||||
getWidget,
|
||||
getWidgets,
|
||||
resolveWidget,
|
||||
registerEditorComponent,
|
||||
getEditorComponents,
|
||||
registerRemarkPlugin,
|
||||
getRemarkPlugins,
|
||||
registerWidgetValueSerializer,
|
||||
getWidgetValueSerializer,
|
||||
registerBackend,
|
||||
@ -91,8 +88,24 @@ export default {
|
||||
getIcon,
|
||||
registerAdditionalLink,
|
||||
getAdditionalLinks,
|
||||
registerPreviewStyle,
|
||||
getPreviewStyles,
|
||||
};
|
||||
|
||||
/**
|
||||
* Preview Styles
|
||||
*
|
||||
* Valid options:
|
||||
* - raw {boolean} if `true`, `style` value is expected to be a CSS string
|
||||
*/
|
||||
export function registerPreviewStyle(style: string, { raw = false }: PreviewStyleOptions = {}) {
|
||||
registry.previewStyles.push({ value: style, raw });
|
||||
}
|
||||
|
||||
export function getPreviewStyles() {
|
||||
return registry.previewStyles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview Templates
|
||||
*/
|
||||
@ -196,44 +209,6 @@ export function resolveWidget<T = unknown, F extends Field = Field>(name?: strin
|
||||
return getWidget(name || 'string') || getWidget('unknown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Markdown Editor Custom Components
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function registerEditorComponent(component: EditorComponentOptions<any>) {
|
||||
const plugin = EditorComponent(component);
|
||||
if ('type' in plugin && plugin.type === 'code-block') {
|
||||
const codeBlock = Object.values(registry.editorComponents).find(
|
||||
c => 'type' in c && c.type === 'code-block',
|
||||
);
|
||||
|
||||
if (codeBlock) {
|
||||
console.warn(oneLine`
|
||||
Only one editor component of type "code-block" may be registered. Previously registered code
|
||||
block component(s) will be overwritten.
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
registry.editorComponents[plugin.id] = plugin;
|
||||
}
|
||||
|
||||
export function getEditorComponents(): Record<string, EditorComponentOptions> {
|
||||
return registry.editorComponents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugins
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function registerRemarkPlugin(plugin: Pluggable) {
|
||||
registry.remarkPlugins.push(plugin);
|
||||
}
|
||||
|
||||
export function getRemarkPlugins(): Pluggable[] {
|
||||
return registry.remarkPlugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Widget Serializers
|
||||
*/
|
||||
@ -383,3 +358,14 @@ export function getAdditionalLinks(): Record<string, AdditionalLink> {
|
||||
export function getAdditionalLink(id: string): AdditionalLink | undefined {
|
||||
return registry.additionalLinks[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Markdown editor options
|
||||
*/
|
||||
export function setMarkdownEditorOptions(options: MarkdownEditorOptions) {
|
||||
registry.markdownEditorConfig = options;
|
||||
}
|
||||
|
||||
export function getMarkdownEditorOptions(): MarkdownEditorOptions {
|
||||
return registry.markdownEditorConfig;
|
||||
}
|
||||
|
@ -72,6 +72,7 @@ const en: LocalePhrasesRoot = {
|
||||
label: 'Updated On',
|
||||
},
|
||||
},
|
||||
notFound: 'Collection not found',
|
||||
},
|
||||
editor: {
|
||||
editorControl: {
|
||||
|
5
core/src/types/markdown.d.ts
vendored
5
core/src/types/markdown.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
declare module 'mdast-util-definitions';
|
||||
declare module 'unist-builder';
|
||||
declare module 'rehype-stringify';
|
||||
declare module 'remark-parse';
|
||||
declare module 'remark-rehype';
|
@ -1,55 +0,0 @@
|
||||
import isFunction from 'lodash/isFunction';
|
||||
|
||||
import { isEditorComponentWidgetOptions } from '../interface';
|
||||
|
||||
import type { EditorComponentOptions } from '../interface';
|
||||
|
||||
const catchesNothing = /.^/;
|
||||
|
||||
function bind(fn: unknown) {
|
||||
return isFunction(fn) && fn.bind(null);
|
||||
}
|
||||
|
||||
export default function createEditorComponent(
|
||||
options: EditorComponentOptions,
|
||||
): EditorComponentOptions {
|
||||
if (isEditorComponentWidgetOptions(options)) {
|
||||
const {
|
||||
id = null,
|
||||
label = 'unnamed component',
|
||||
type = 'shortcode',
|
||||
widget = 'object',
|
||||
...remainingConfig
|
||||
} = options;
|
||||
|
||||
return {
|
||||
id: id || label.replace(/[^A-Z0-9]+/gi, '_'),
|
||||
label,
|
||||
type,
|
||||
widget,
|
||||
...remainingConfig,
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
id = null,
|
||||
label = 'unnamed component',
|
||||
pattern = catchesNothing,
|
||||
fields = [],
|
||||
fromBlock,
|
||||
toBlock,
|
||||
toPreview,
|
||||
...remainingConfig
|
||||
} = options;
|
||||
|
||||
return {
|
||||
id: id || label.replace(/[^A-Z0-9]+/gi, '_'),
|
||||
label,
|
||||
pattern,
|
||||
fromBlock: bind(fromBlock) || (() => ({})),
|
||||
toBlock: bind(toBlock) || (() => 'Plugin'),
|
||||
toPreview: bind(toPreview) || bind(toBlock) || (() => 'Plugin'),
|
||||
fields,
|
||||
...remainingConfig,
|
||||
};
|
||||
}
|
@ -88,8 +88,6 @@ const settingsPersistKeys = {
|
||||
};
|
||||
|
||||
const CodeControl = ({
|
||||
isEditorComponent,
|
||||
isNewEditorComponent,
|
||||
field,
|
||||
onChange,
|
||||
hasErrors,
|
||||
@ -112,31 +110,20 @@ const CodeControl = ({
|
||||
);
|
||||
|
||||
// If the value is a map, keys can be customized via config.
|
||||
const getKeys = useCallback(
|
||||
(field: CodeField) => {
|
||||
const defaults = {
|
||||
code: 'code',
|
||||
lang: 'lang',
|
||||
};
|
||||
const getKeys = useCallback((field: CodeField) => {
|
||||
const defaults = {
|
||||
code: 'code',
|
||||
lang: 'lang',
|
||||
};
|
||||
|
||||
// Force default keys if widget is an editor component code block.
|
||||
if (isEditorComponent) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
const keys = field.keys ?? {};
|
||||
return { ...defaults, ...keys };
|
||||
},
|
||||
[isEditorComponent],
|
||||
);
|
||||
const keys = field.keys ?? {};
|
||||
return { ...defaults, ...keys };
|
||||
}, []);
|
||||
|
||||
const keys = useMemo(() => getKeys(field), [field, getKeys]);
|
||||
|
||||
// Determine if the persisted value is a map rather than a plain string. A map value allows both the code string and the language to be persisted.
|
||||
const valueIsMap = useMemo(
|
||||
() => Boolean(!field.output_code_only || isEditorComponent),
|
||||
[field.output_code_only, isEditorComponent],
|
||||
);
|
||||
const valueIsMap = useMemo(() => Boolean(!field.output_code_only), [field.output_code_only]);
|
||||
|
||||
// This widget is not fully controlled, it only takes a value through props upon initialization.
|
||||
const getInitialLang = useCallback(() => {
|
||||
@ -362,9 +349,6 @@ const CodeControl = ({
|
||||
detach={true}
|
||||
editorDidMount={cm => {
|
||||
setCodemirrorEditor(cm);
|
||||
if (isNewEditorComponent) {
|
||||
handleFocus();
|
||||
}
|
||||
}}
|
||||
value={lastKnownValue}
|
||||
onChange={(_editor, _data, newValue) => handleChange(newValue)}
|
||||
|
@ -7,18 +7,18 @@ import uuid from 'uuid';
|
||||
import FieldLabel from '../../components/UI/FieldLabel';
|
||||
import Outline from '../../components/UI/Outline';
|
||||
import { IMAGE_EXTENSION_REGEX } from '../../constants/files';
|
||||
import useImagePlugin from '../../editor-components/editorPlugin';
|
||||
import { doesUrlFileExist } from '../../lib/util/fetch.util';
|
||||
import { isNotNullish } from '../../lib/util/null.util';
|
||||
import { isNotEmpty } from '../../lib/util/string.util';
|
||||
import useEditorOptions from './hooks/useEditorOptions';
|
||||
import useToolbarItems from './hooks/useToolbarItems';
|
||||
import useWidgetRules from './hooks/useWidgetRules';
|
||||
|
||||
import type { RefObject } from 'react';
|
||||
import type { MarkdownField, MediaLibrary, WidgetControlProps } from '../../interface';
|
||||
|
||||
import '@toast-ui/editor/dist/toastui-editor.css';
|
||||
|
||||
const imageFilePattern = /(!)?\[([^\]]*)\]\(([^)]+)\)/;
|
||||
|
||||
const StyledEditorWrapper = styled('div')`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@ -91,10 +91,6 @@ const MarkdownControl = ({
|
||||
[controlID, field, mediaLibraryFieldOptions, openMediaLibrary],
|
||||
);
|
||||
|
||||
const imageToolbarButton = useImagePlugin({
|
||||
openMediaLibrary: handleOpenMedialLibrary,
|
||||
});
|
||||
|
||||
const getMedia = useCallback(
|
||||
async (path: string) => {
|
||||
const { type, exists } = await doesUrlFileExist(path);
|
||||
@ -155,6 +151,10 @@ const MarkdownControl = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [field, mediaPath]);
|
||||
|
||||
const { initialEditType, height, plugins, ...markdownEditorOptions } = useEditorOptions();
|
||||
const widgetRules = useWidgetRules(markdownEditorOptions.widgetRules, { getAsset, field });
|
||||
const toolbarItems = useToolbarItems(markdownEditorOptions.toolbarItems, handleOpenMedialLibrary);
|
||||
|
||||
return (
|
||||
<StyledEditorWrapper key="markdown-control-wrapper">
|
||||
<FieldLabel
|
||||
@ -169,50 +169,17 @@ const MarkdownControl = ({
|
||||
key="markdown-control-editor"
|
||||
initialValue={internalValue}
|
||||
previewStyle="vertical"
|
||||
height="600px"
|
||||
initialEditType="markdown"
|
||||
height={height}
|
||||
initialEditType={initialEditType}
|
||||
useCommandShortcut={true}
|
||||
onChange={handleOnChange}
|
||||
toolbarItems={[
|
||||
['heading', 'bold', 'italic', 'strike'],
|
||||
['hr', 'quote'],
|
||||
['ul', 'ol', 'task', 'indent', 'outdent'],
|
||||
['table', imageToolbarButton, 'link'],
|
||||
['code', 'codeblock'],
|
||||
]}
|
||||
toolbarItems={toolbarItems}
|
||||
ref={editorRef}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
autofocus={false}
|
||||
widgetRules={[
|
||||
{
|
||||
rule: imageFilePattern,
|
||||
toDOM(text) {
|
||||
const rule = imageFilePattern;
|
||||
const matched = text.match(rule);
|
||||
|
||||
if (matched) {
|
||||
if (matched?.length === 4) {
|
||||
// Image
|
||||
const img = document.createElement('img');
|
||||
img.setAttribute('src', getAsset(matched[3], field).url);
|
||||
img.setAttribute('style', 'width: 100%;');
|
||||
img.innerHTML = 'test';
|
||||
return img;
|
||||
} else {
|
||||
// File
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('href', matched[2]);
|
||||
a.innerHTML = matched[1];
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
return document.createElement('div');
|
||||
},
|
||||
},
|
||||
]}
|
||||
widgetRules={widgetRules}
|
||||
plugins={plugins}
|
||||
/>
|
||||
<Outline key="markdown-control-outline" hasLabel hasError={hasErrors} />
|
||||
</StyledEditorWrapper>
|
||||
|
@ -1,28 +1,28 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import React from 'react';
|
||||
import { Viewer } from '@toast-ui/react-editor';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import WidgetPreviewContainer from '../../components/UI/WidgetPreviewContainer';
|
||||
import { markdownToHtml } from './serializers';
|
||||
import useEditorOptions from './hooks/useEditorOptions';
|
||||
|
||||
import type { MarkdownField, WidgetPreviewProps } from '../../interface';
|
||||
|
||||
const MarkdownPreview = ({
|
||||
value,
|
||||
getAsset,
|
||||
field,
|
||||
getRemarkPlugins,
|
||||
}: WidgetPreviewProps<string, MarkdownField>) => {
|
||||
const MarkdownPreview = ({ value }: WidgetPreviewProps<string, MarkdownField>) => {
|
||||
const { plugins } = useEditorOptions();
|
||||
const viewer = useRef<Viewer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
viewer.current?.getInstance().setMarkdown(value ?? '');
|
||||
}, [value]);
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const html = markdownToHtml(value, {
|
||||
getAsset,
|
||||
remarkPlugins: getRemarkPlugins(),
|
||||
});
|
||||
const toRender = field.sanitize_preview ?? false ? DOMPurify.sanitize(html) : html;
|
||||
|
||||
return <WidgetPreviewContainer dangerouslySetInnerHTML={{ __html: toRender }} />;
|
||||
return (
|
||||
<WidgetPreviewContainer>
|
||||
<Viewer ref={viewer} initialValue={value} plugins={plugins} />
|
||||
</WidgetPreviewContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownPreview;
|
||||
|
35
core/src/widgets/markdown/config/widgetRules.ts
Normal file
35
core/src/widgets/markdown/config/widgetRules.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { WidgetRulesFactory } from '../../../interface';
|
||||
|
||||
const imageFilePattern = /(!)?\[([^\]]*)\]\(([^)]+)\)/;
|
||||
|
||||
const defaultWidgetRules: WidgetRulesFactory = ({ getAsset, field }) => [
|
||||
{
|
||||
rule: imageFilePattern,
|
||||
toDOM(text) {
|
||||
const rule = imageFilePattern;
|
||||
const matched = text.match(rule);
|
||||
|
||||
if (matched) {
|
||||
if (matched?.length === 4) {
|
||||
// Image
|
||||
const img = document.createElement('img');
|
||||
img.setAttribute('src', getAsset(matched[3], field).url);
|
||||
img.setAttribute('style', 'width: 100%;');
|
||||
img.innerHTML = 'test';
|
||||
return img;
|
||||
} else {
|
||||
// File
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('href', matched[2]);
|
||||
a.innerHTML = matched[1];
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
return document.createElement('div');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default defaultWidgetRules;
|
23
core/src/widgets/markdown/hooks/useEditorOptions.ts
Normal file
23
core/src/widgets/markdown/hooks/useEditorOptions.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { getMarkdownEditorOptions } from '../../../lib/registry';
|
||||
|
||||
const useEditorOptions = () => {
|
||||
return useMemo(() => {
|
||||
const {
|
||||
initialEditType = 'wysiwyg',
|
||||
height = '600px',
|
||||
plugins = [],
|
||||
...markdownEditorOptions
|
||||
} = getMarkdownEditorOptions();
|
||||
|
||||
return {
|
||||
initialEditType,
|
||||
height,
|
||||
plugins,
|
||||
...markdownEditorOptions,
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useEditorOptions;
|
32
core/src/widgets/markdown/hooks/useToolbarItems.ts
Normal file
32
core/src/widgets/markdown/hooks/useToolbarItems.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import useImageToolbarButton from '../toolbar/useImageToolbarButton';
|
||||
|
||||
import type { MarkdownEditorOptions } from '../../../interface';
|
||||
|
||||
const useToolbarItems = (
|
||||
toolbarItems: MarkdownEditorOptions['toolbarItems'],
|
||||
openMediaLibrary: (forImage: boolean) => void,
|
||||
) => {
|
||||
const imageToolbarButton = useImageToolbarButton({
|
||||
openMediaLibrary,
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
let items = [
|
||||
['heading', 'bold', 'italic', 'strike'],
|
||||
['hr', 'quote'],
|
||||
['ul', 'ol', 'task', 'indent', 'outdent'],
|
||||
['table', imageToolbarButton, 'link'],
|
||||
['code', 'codeblock'],
|
||||
];
|
||||
|
||||
if (toolbarItems) {
|
||||
items = toolbarItems({ imageToolbarButton });
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [imageToolbarButton, toolbarItems]);
|
||||
};
|
||||
|
||||
export default useToolbarItems;
|
20
core/src/widgets/markdown/hooks/useWidgetRules.ts
Normal file
20
core/src/widgets/markdown/hooks/useWidgetRules.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import defaultWidgetRules from '../config/widgetRules';
|
||||
|
||||
import type { WidgetRulesFactory, WidgetRulesFactoryProps } from '../../../interface';
|
||||
|
||||
const useWidgetRules = (
|
||||
widgetRules: WidgetRulesFactory | undefined,
|
||||
{ getAsset, field }: WidgetRulesFactoryProps,
|
||||
) => {
|
||||
return useMemo(() => {
|
||||
const rules = defaultWidgetRules({ getAsset, field });
|
||||
if (widgetRules) {
|
||||
rules.push(...widgetRules({ getAsset, field }));
|
||||
}
|
||||
return rules;
|
||||
}, [field, getAsset, widgetRules]);
|
||||
};
|
||||
|
||||
export default useWidgetRules;
|
@ -1,40 +0,0 @@
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkParse from 'remark-parse';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import { unified } from 'unified';
|
||||
|
||||
// import { getEditorComponents } from '../../../lib/registry';
|
||||
|
||||
import type { Pluggable } from 'unified';
|
||||
import type { GetAssetFunction } from '../../interface';
|
||||
|
||||
interface MarkdownToHtmlProps {
|
||||
getAsset: GetAssetFunction;
|
||||
remarkPlugins?: Pluggable[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Markdown to HTML.
|
||||
*/
|
||||
export function markdownToHtml(
|
||||
markdown: string,
|
||||
{ remarkPlugins = [] }: MarkdownToHtmlProps,
|
||||
): string {
|
||||
const html = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
// .use(remarkParseShortcodes as any, { plugins: getEditorComponents() })
|
||||
.use(remarkPlugins)
|
||||
// .use(remarkToRehypeShortcodes as any, { plugins: getEditorComponents(), getAsset })
|
||||
.use(remarkRehype, { allowDangerousHTML: true })
|
||||
.use(rehypeStringify, {
|
||||
allowDangerousHtml: true,
|
||||
allowDangerousCharacters: true,
|
||||
closeSelfClosing: true,
|
||||
entities: { useNamedReferences: true },
|
||||
})
|
||||
.processSync(markdown);
|
||||
|
||||
return String(html);
|
||||
}
|
@ -7,7 +7,7 @@ export interface ImagePluginProps {
|
||||
|
||||
const PREFIX = 'toastui-editor-';
|
||||
|
||||
const useImagePlugin = ({ openMediaLibrary }: ImagePluginProps): ToolbarItemOptions => {
|
||||
const useImageToolbarButton = ({ openMediaLibrary }: ImagePluginProps): ToolbarItemOptions => {
|
||||
const toolbarButton = useMemo(() => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
@ -41,4 +41,4 @@ const useImagePlugin = ({ openMediaLibrary }: ImagePluginProps): ToolbarItemOpti
|
||||
return toolbarItem;
|
||||
};
|
||||
|
||||
export default useImagePlugin;
|
||||
export default useImageToolbarButton;
|
Reference in New Issue
Block a user