feat: add markdown shortcodes (#215)
This commit is contained in:
parent
388de1e0c4
commit
bb84382f6e
2
.github/workflows/core.yml
vendored
2
.github/workflows/core.yml
vendored
@ -79,4 +79,4 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
working-directory: ./core
|
working-directory: ./core
|
||||||
run: |
|
run: |
|
||||||
yarn test
|
yarn test:ci
|
||||||
|
@ -174,13 +174,13 @@ widget: 'markdown',
|
|||||||
|
|
||||||
## Formatting
|
## Formatting
|
||||||
|
|
||||||
<font style={{ color: 'red', backgroundColor: 'black' }}>Colored Text</font>
|
**Bold**, *Italic*, ***both***
|
||||||
|
|
||||||
<p align="center">Centered Text</p>
|
~~Strikethrough~~
|
||||||
|
|
||||||
**Bold**, *Italic*, ***both***, <u>Underlined</u>
|
## Shortcodes
|
||||||
|
|
||||||
~~Strikethrough~~, <sub>subscript</sub>, <sup>superscript</sup>
|
[youtube|p6h-rYSVX90]
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
|
@ -118,3 +118,56 @@ CMS.registerAdditionalLink({
|
|||||||
icon: 'page',
|
icon: 'page',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CMS.registerShortcode('youtube', {
|
||||||
|
label: 'YouTube',
|
||||||
|
openTag: '[',
|
||||||
|
closeTag: ']',
|
||||||
|
separator: '|',
|
||||||
|
toProps: args => {
|
||||||
|
if (args.length > 0) {
|
||||||
|
return { src: args[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { src: '' };
|
||||||
|
},
|
||||||
|
toArgs: ({ src }) => {
|
||||||
|
return [src];
|
||||||
|
},
|
||||||
|
control: ({ src, onChange }) => {
|
||||||
|
return h('span', {}, [
|
||||||
|
h('input', {
|
||||||
|
key: 'control-input',
|
||||||
|
value: src,
|
||||||
|
onChange: event => {
|
||||||
|
onChange({ src: event.target.value });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
h(
|
||||||
|
'iframe',
|
||||||
|
{
|
||||||
|
key: 'control-preview',
|
||||||
|
width: '420',
|
||||||
|
height: '315',
|
||||||
|
src: `https://www.youtube.com/embed/${src}`,
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
preview: ({ src }) => {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{},
|
||||||
|
h(
|
||||||
|
'iframe',
|
||||||
|
{
|
||||||
|
width: '420',
|
||||||
|
height: '315',
|
||||||
|
src: `https://www.youtube.com/embed/${src}`,
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -12,6 +12,6 @@ module.exports = {
|
|||||||
...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/' }),
|
...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/' }),
|
||||||
'\\.(css|less)$': '<rootDir>/src/__mocks__/styleMock.ts',
|
'\\.(css|less)$': '<rootDir>/src/__mocks__/styleMock.ts',
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: ['node_modules/(?!(url-join|array-move|ol)/)'],
|
transformIgnorePatterns: [],
|
||||||
setupFiles: ['./test/setupEnv.js'],
|
setupFiles: ['./test/setupEnv.js'],
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
"prepublishOnly": "yarn build",
|
"prepublishOnly": "yarn build",
|
||||||
"start": "run-s clean develop",
|
"start": "run-s clean develop",
|
||||||
"test": "cross-env NODE_ENV=test jest",
|
"test": "cross-env NODE_ENV=test jest",
|
||||||
|
"test:ci": "cross-env NODE_ENV=test jest --maxWorkers=2",
|
||||||
"type-check": "tsc --watch"
|
"type-check": "tsc --watch"
|
||||||
},
|
},
|
||||||
"main": "dist/static-cms-core.js",
|
"main": "dist/static-cms-core.js",
|
||||||
|
45
core/src/__mocks__/@udecode/plate.ts
Normal file
45
core/src/__mocks__/@udecode/plate.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
ELEMENT_BLOCKQUOTE,
|
||||||
|
ELEMENT_CODE_BLOCK,
|
||||||
|
ELEMENT_H1,
|
||||||
|
ELEMENT_H2,
|
||||||
|
ELEMENT_H3,
|
||||||
|
ELEMENT_H4,
|
||||||
|
ELEMENT_H5,
|
||||||
|
ELEMENT_H6,
|
||||||
|
ELEMENT_IMAGE,
|
||||||
|
ELEMENT_LI,
|
||||||
|
ELEMENT_LIC,
|
||||||
|
ELEMENT_LINK,
|
||||||
|
ELEMENT_OL,
|
||||||
|
ELEMENT_PARAGRAPH,
|
||||||
|
ELEMENT_TABLE,
|
||||||
|
ELEMENT_TD,
|
||||||
|
ELEMENT_TH,
|
||||||
|
ELEMENT_TR,
|
||||||
|
ELEMENT_UL,
|
||||||
|
} from '@udecode/plate';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ELEMENT_BLOCKQUOTE,
|
||||||
|
ELEMENT_CODE_BLOCK,
|
||||||
|
ELEMENT_H1,
|
||||||
|
ELEMENT_H2,
|
||||||
|
ELEMENT_H3,
|
||||||
|
ELEMENT_H4,
|
||||||
|
ELEMENT_H5,
|
||||||
|
ELEMENT_H6,
|
||||||
|
ELEMENT_IMAGE,
|
||||||
|
ELEMENT_LI,
|
||||||
|
ELEMENT_LIC,
|
||||||
|
ELEMENT_LINK,
|
||||||
|
ELEMENT_OL,
|
||||||
|
ELEMENT_PARAGRAPH,
|
||||||
|
ELEMENT_TABLE,
|
||||||
|
ELEMENT_TD,
|
||||||
|
ELEMENT_TH,
|
||||||
|
ELEMENT_TR,
|
||||||
|
ELEMENT_UL,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {};
|
@ -18,6 +18,7 @@ import {
|
|||||||
ListWidget,
|
ListWidget,
|
||||||
MapWidget,
|
MapWidget,
|
||||||
MarkdownWidget,
|
MarkdownWidget,
|
||||||
|
MdxWidget,
|
||||||
NumberWidget,
|
NumberWidget,
|
||||||
ObjectWidget,
|
ObjectWidget,
|
||||||
RelationWidget,
|
RelationWidget,
|
||||||
@ -44,6 +45,7 @@ export default function addExtensions() {
|
|||||||
ListWidget(),
|
ListWidget(),
|
||||||
MapWidget(),
|
MapWidget(),
|
||||||
MarkdownWidget(),
|
MarkdownWidget(),
|
||||||
|
MdxWidget(),
|
||||||
NumberWidget(),
|
NumberWidget(),
|
||||||
ObjectWidget(),
|
ObjectWidget(),
|
||||||
RelationWidget(),
|
RelationWidget(),
|
||||||
|
@ -889,6 +889,26 @@ export interface MarkdownEditorOptions {
|
|||||||
plugins?: MarkdownPluginFactory[];
|
plugins?: MarkdownPluginFactory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ShortcodeControlProps<P = {}> = P & {
|
||||||
|
onChange: (props: P) => void;
|
||||||
|
controlProps: WidgetControlProps<string, MarkdownField>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShortcodePreviewProps<P = {}> = P & {
|
||||||
|
previewProps: WidgetPreviewProps<string, MarkdownField>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ShortcodeConfig<P = {}> {
|
||||||
|
label?: string;
|
||||||
|
openTag: string;
|
||||||
|
closeTag: string;
|
||||||
|
separator: string;
|
||||||
|
toProps?: (args: string[]) => P;
|
||||||
|
toArgs?: (props: P) => string[];
|
||||||
|
control: ComponentType<ShortcodeControlProps>;
|
||||||
|
preview: ComponentType<ShortcodePreviewProps>;
|
||||||
|
}
|
||||||
|
|
||||||
export enum CollectionType {
|
export enum CollectionType {
|
||||||
FOLDER,
|
FOLDER,
|
||||||
FILES,
|
FILES,
|
||||||
|
@ -14,11 +14,11 @@ import type {
|
|||||||
EventListener,
|
EventListener,
|
||||||
Field,
|
Field,
|
||||||
LocalePhrasesRoot,
|
LocalePhrasesRoot,
|
||||||
MarkdownEditorOptions,
|
|
||||||
MediaLibraryExternalLibrary,
|
MediaLibraryExternalLibrary,
|
||||||
MediaLibraryOptions,
|
MediaLibraryOptions,
|
||||||
PreviewStyle,
|
PreviewStyle,
|
||||||
PreviewStyleOptions,
|
PreviewStyleOptions,
|
||||||
|
ShortcodeConfig,
|
||||||
TemplatePreviewComponent,
|
TemplatePreviewComponent,
|
||||||
UnknownField,
|
UnknownField,
|
||||||
Widget,
|
Widget,
|
||||||
@ -48,7 +48,7 @@ interface Registry {
|
|||||||
previewStyles: PreviewStyle[];
|
previewStyles: PreviewStyle[];
|
||||||
|
|
||||||
/** Markdown editor */
|
/** Markdown editor */
|
||||||
markdownEditorConfig: MarkdownEditorOptions;
|
shortcodes: Record<string, ShortcodeConfig>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,7 +65,7 @@ const registry: Registry = {
|
|||||||
locales: {},
|
locales: {},
|
||||||
eventHandlers,
|
eventHandlers,
|
||||||
previewStyles: [],
|
previewStyles: [],
|
||||||
markdownEditorConfig: {},
|
shortcodes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -93,6 +93,9 @@ export default {
|
|||||||
getAdditionalLinks,
|
getAdditionalLinks,
|
||||||
registerPreviewStyle,
|
registerPreviewStyle,
|
||||||
getPreviewStyles,
|
getPreviewStyles,
|
||||||
|
registerShortcode,
|
||||||
|
getShortcode,
|
||||||
|
getShortcodes,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,7 +136,7 @@ export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
|||||||
options?: WidgetOptions<T, F>,
|
options?: WidgetOptions<T, F>,
|
||||||
): void;
|
): void;
|
||||||
export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
||||||
name: string | WidgetParam<T, F> | WidgetParam[],
|
nameOrWidgetOrWidgets: string | WidgetParam<T, F> | WidgetParam[],
|
||||||
control?: string | Widget<T, F>['control'],
|
control?: string | Widget<T, F>['control'],
|
||||||
preview?: Widget<T, F>['preview'],
|
preview?: Widget<T, F>['preview'],
|
||||||
{
|
{
|
||||||
@ -143,22 +146,22 @@ export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
|||||||
getDefaultValue,
|
getDefaultValue,
|
||||||
}: WidgetOptions<T, F> = {},
|
}: WidgetOptions<T, F> = {},
|
||||||
): void {
|
): void {
|
||||||
if (Array.isArray(name)) {
|
if (Array.isArray(nameOrWidgetOrWidgets)) {
|
||||||
name.forEach(widget => {
|
nameOrWidgetOrWidgets.forEach(widget => {
|
||||||
if (typeof widget !== 'object') {
|
if (typeof widget !== 'object') {
|
||||||
console.error(`Cannot register widget: ${widget}`);
|
console.error(`Cannot register widget: ${widget}`);
|
||||||
} else {
|
} else {
|
||||||
registerWidget(widget);
|
registerWidget(widget);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (typeof name === 'string') {
|
} else if (typeof nameOrWidgetOrWidgets === 'string') {
|
||||||
// A registered widget control can be reused by a new widget, allowing
|
// A registered widget control can be reused by a new widget, allowing
|
||||||
// multiple copies with different previews.
|
// multiple copies with different previews.
|
||||||
const newControl = (
|
const newControl = (
|
||||||
typeof control === 'string' ? registry.widgets[control]?.control : control
|
typeof control === 'string' ? registry.widgets[control]?.control : control
|
||||||
) as Widget['control'];
|
) as Widget['control'];
|
||||||
if (newControl) {
|
if (newControl) {
|
||||||
registry.widgets[name] = {
|
registry.widgets[nameOrWidgetOrWidgets] = {
|
||||||
control: newControl,
|
control: newControl,
|
||||||
preview: preview as Widget['preview'],
|
preview: preview as Widget['preview'],
|
||||||
validator: validator as Widget['validator'],
|
validator: validator as Widget['validator'],
|
||||||
@ -167,7 +170,7 @@ export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
|||||||
schema,
|
schema,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (typeof name === 'object') {
|
} else if (typeof nameOrWidgetOrWidgets === 'object') {
|
||||||
const {
|
const {
|
||||||
name: widgetName,
|
name: widgetName,
|
||||||
controlComponent: control,
|
controlComponent: control,
|
||||||
@ -178,7 +181,7 @@ export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
|||||||
getDefaultValue,
|
getDefaultValue,
|
||||||
schema,
|
schema,
|
||||||
} = {},
|
} = {},
|
||||||
} = name;
|
} = nameOrWidgetOrWidgets;
|
||||||
if (registry.widgets[widgetName]) {
|
if (registry.widgets[widgetName]) {
|
||||||
console.warn(oneLine`
|
console.warn(oneLine`
|
||||||
Multiple widgets registered with name "${widgetName}". Only the last widget registered with
|
Multiple widgets registered with name "${widgetName}". Only the last widget registered with
|
||||||
@ -369,12 +372,20 @@ export function getAdditionalLink(id: string): AdditionalLink | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Markdown editor options
|
* Markdown editor shortcodes
|
||||||
*/
|
*/
|
||||||
export function setMarkdownEditorOptions(options: MarkdownEditorOptions) {
|
export function registerShortcode(name: string, config: ShortcodeConfig) {
|
||||||
registry.markdownEditorConfig = options;
|
if (registry.backends[name]) {
|
||||||
|
console.error(`Shortcode [${name}] already registered. Please choose a different name.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registry.shortcodes[name] = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMarkdownEditorOptions(): MarkdownEditorOptions {
|
export function getShortcode(name: string): ShortcodeConfig {
|
||||||
return registry.markdownEditorConfig;
|
return registry.shortcodes[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getShortcodes(): Record<string, ShortcodeConfig> {
|
||||||
|
return registry.shortcodes;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ export * from './map';
|
|||||||
export { default as MapWidget } from './map';
|
export { default as MapWidget } from './map';
|
||||||
export * from './markdown';
|
export * from './markdown';
|
||||||
export { default as MarkdownWidget } from './markdown';
|
export { default as MarkdownWidget } from './markdown';
|
||||||
|
export * from './mdx';
|
||||||
|
export { default as MdxWidget } from './mdx';
|
||||||
export * from './number';
|
export * from './number';
|
||||||
export { default as NumberWidget } from './number';
|
export { default as NumberWidget } from './number';
|
||||||
export * from './object';
|
export * from './object';
|
||||||
|
@ -1,113 +0,0 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import FieldLabel from '@staticcms/core/components/UI/FieldLabel';
|
|
||||||
import Outline from '@staticcms/core/components/UI/Outline';
|
|
||||||
import useDebounce from '../../lib/hooks/useDebounce';
|
|
||||||
import useMarkdownToSlate from './plate/hooks/useMarkdownToSlate';
|
|
||||||
import PlateEditor from './plate/PlateEditor';
|
|
||||||
import serialize from './plate/serialization/serializerMarkdown';
|
|
||||||
|
|
||||||
import type { MarkdownField, WidgetControlProps } from '@staticcms/core/interface';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import type { MdValue } from './plate/plateTypes';
|
|
||||||
import type { BlockType, LeafType } from './plate/serialization/slate/ast-types';
|
|
||||||
|
|
||||||
const StyledEditorWrapper = styled('div')`
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.toastui-editor-main .toastui-editor-md-vertical-style .toastui-editor {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toastui-editor-main .toastui-editor-md-splitter {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toastui-editor-md-preview {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toastui-editor-defaultUI {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MarkdownControl: FC<WidgetControlProps<string, MarkdownField>> = ({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
hasErrors,
|
|
||||||
collection,
|
|
||||||
entry,
|
|
||||||
field,
|
|
||||||
}) => {
|
|
||||||
const [internalValue, setInternalValue] = useState(value ?? '');
|
|
||||||
const [hasFocus, setHasFocus] = useState(false);
|
|
||||||
const debouncedFocus = useDebounce(hasFocus, 150);
|
|
||||||
|
|
||||||
const handleOnFocus = useCallback(() => {
|
|
||||||
setHasFocus(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOnBlur = useCallback(() => {
|
|
||||||
setHasFocus(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOnChange = useCallback(
|
|
||||||
(slateValue: MdValue) => {
|
|
||||||
const newValue = slateValue.map(v => serialize(v as BlockType | LeafType)).join('\n');
|
|
||||||
// console.log('[Plate] slateValue', slateValue, 'newMarkdownValue', newValue);
|
|
||||||
if (newValue !== internalValue) {
|
|
||||||
setInternalValue(newValue);
|
|
||||||
onChange(newValue);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[internalValue, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleLabelClick = useCallback(() => {
|
|
||||||
// editorRef.current?.getInstance().focus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [slateValue, loaded] = useMarkdownToSlate(internalValue);
|
|
||||||
|
|
||||||
// console.log('[Plate] slateValue', slateValue);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => (
|
|
||||||
<StyledEditorWrapper key="markdown-control-wrapper">
|
|
||||||
<FieldLabel
|
|
||||||
key="markdown-control-label"
|
|
||||||
isActive={hasFocus}
|
|
||||||
hasErrors={hasErrors}
|
|
||||||
onClick={handleLabelClick}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</FieldLabel>
|
|
||||||
{loaded ? (
|
|
||||||
<PlateEditor
|
|
||||||
initialValue={slateValue}
|
|
||||||
collection={collection}
|
|
||||||
entry={entry}
|
|
||||||
field={field}
|
|
||||||
onChange={handleOnChange}
|
|
||||||
onFocus={handleOnFocus}
|
|
||||||
onBlur={handleOnBlur}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<Outline
|
|
||||||
key="markdown-control-outline"
|
|
||||||
hasLabel
|
|
||||||
hasError={hasErrors}
|
|
||||||
active={hasFocus || debouncedFocus}
|
|
||||||
/>
|
|
||||||
</StyledEditorWrapper>
|
|
||||||
),
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[hasErrors, hasFocus, label, loaded, slateValue],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MarkdownControl;
|
|
@ -3,10 +3,13 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { VFileMessage } from 'vfile-message';
|
import { VFileMessage } from 'vfile-message';
|
||||||
|
|
||||||
import WidgetPreviewContainer from '@staticcms/core/components/UI/WidgetPreviewContainer';
|
import WidgetPreviewContainer from '@staticcms/core/components/UI/WidgetPreviewContainer';
|
||||||
|
import { getShortcodes } from '../../lib/registry';
|
||||||
|
import withShortcodeMdxComponent from './mdx/withShortcodeMdxComponent';
|
||||||
import useMdx from './plate/hooks/useMdx';
|
import useMdx from './plate/hooks/useMdx';
|
||||||
|
import { processShortcodeConfigToMdx } from './plate/serialization/slate/processShortcodeConfig';
|
||||||
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
|
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
interface FallbackComponentProps {
|
interface FallbackComponentProps {
|
||||||
error: string;
|
error: string;
|
||||||
@ -22,18 +25,23 @@ function FallbackComponent({ error }: FallbackComponentProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = ({ value }) => {
|
const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = previewProps => {
|
||||||
useEffect(() => {
|
const { value } = previewProps;
|
||||||
// viewer.current?.getInstance().setMarkdown(value ?? '');
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
const components = useMemo(
|
||||||
}, [value]);
|
() => ({
|
||||||
|
Shortcode: withShortcodeMdxComponent({ previewProps }),
|
||||||
|
}),
|
||||||
|
[previewProps],
|
||||||
|
);
|
||||||
|
|
||||||
const [state, setValue] = useMdx(value ?? '');
|
const [state, setValue] = useMdx(value ?? '');
|
||||||
const [prevValue, setPrevValue] = useState(value);
|
const [prevValue, setPrevValue] = useState('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prevValue !== value) {
|
if (prevValue !== value) {
|
||||||
setPrevValue(value ?? '');
|
const parsedValue = processShortcodeConfigToMdx(getShortcodes(), value ?? '');
|
||||||
setValue(value ?? '');
|
setPrevValue(parsedValue);
|
||||||
|
setValue(parsedValue);
|
||||||
}
|
}
|
||||||
}, [prevValue, setValue, value]);
|
}, [prevValue, setValue, value]);
|
||||||
|
|
||||||
@ -50,8 +58,6 @@ const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = ({ value
|
|||||||
}
|
}
|
||||||
}, [state.file]);
|
}, [state.file]);
|
||||||
|
|
||||||
const components = useMemo(() => ({}), []);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import controlComponent from './MarkdownControl';
|
import withMarkdownControl from './withMarkdownControl';
|
||||||
import previewComponent from './MarkdownPreview';
|
import previewComponent from './MarkdownPreview';
|
||||||
import schema from './schema';
|
import schema from './schema';
|
||||||
|
|
||||||
import type { MarkdownField, WidgetParam } from '@staticcms/core/interface';
|
import type { MarkdownField, WidgetParam } from '@staticcms/core/interface';
|
||||||
|
|
||||||
|
const controlComponent = withMarkdownControl({ useMdx: false });
|
||||||
|
|
||||||
const MarkdownWidget = (): WidgetParam<string, MarkdownField> => {
|
const MarkdownWidget = (): WidgetParam<string, MarkdownField> => {
|
||||||
return {
|
return {
|
||||||
name: 'markdown',
|
name: 'markdown',
|
||||||
|
2
core/src/widgets/markdown/mdx/index.ts
Normal file
2
core/src/widgets/markdown/mdx/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './withShortcodeMdxComponent';
|
||||||
|
export { default as withShortcodeElement } from './withShortcodeMdxComponent';
|
36
core/src/widgets/markdown/mdx/withShortcodeMdxComponent.tsx
Normal file
36
core/src/widgets/markdown/mdx/withShortcodeMdxComponent.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { getShortcode } from '../../../lib/registry';
|
||||||
|
|
||||||
|
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
export interface WithShortcodeMdxComponentProps {
|
||||||
|
previewProps: WidgetPreviewProps<string, MarkdownField>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShortcodeMdxComponentProps {
|
||||||
|
shortcode: string;
|
||||||
|
args: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const withShortcodeMdxComponent = ({ previewProps }: WithShortcodeMdxComponentProps) => {
|
||||||
|
const ShortcodeMdxComponent: FC<ShortcodeMdxComponentProps> = ({ shortcode, args }) => {
|
||||||
|
const config = useMemo(() => getShortcode(shortcode), [shortcode]);
|
||||||
|
|
||||||
|
const [ShortcodePreview, props] = useMemo(() => {
|
||||||
|
if (!config) {
|
||||||
|
return [null, {}];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = config.toProps ? config.toProps(args) : {};
|
||||||
|
return [config.preview, props];
|
||||||
|
}, [config, args]);
|
||||||
|
|
||||||
|
return ShortcodePreview ? <ShortcodePreview previewProps={previewProps} {...props} /> : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return ShortcodeMdxComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withShortcodeMdxComponent;
|
@ -51,11 +51,12 @@ import {
|
|||||||
withProps,
|
withProps,
|
||||||
} from '@udecode/plate';
|
} from '@udecode/plate';
|
||||||
import { StyledLeaf } from '@udecode/plate-styled-components';
|
import { StyledLeaf } from '@udecode/plate-styled-components';
|
||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useMemo, useRef } from 'react';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
|
||||||
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||||
|
import { withShortcodeElement } from './components';
|
||||||
import { BalloonToolbar } from './components/balloon-toolbar';
|
import { BalloonToolbar } from './components/balloon-toolbar';
|
||||||
import { BlockquoteElement } from './components/nodes/blockquote';
|
import { BlockquoteElement } from './components/nodes/blockquote';
|
||||||
import { CodeBlockElement } from './components/nodes/code-block';
|
import { CodeBlockElement } from './components/nodes/code-block';
|
||||||
@ -75,11 +76,11 @@ import {
|
|||||||
OrderedListElement,
|
OrderedListElement,
|
||||||
UnorderedListElement,
|
UnorderedListElement,
|
||||||
} from './components/nodes/list';
|
} from './components/nodes/list';
|
||||||
import Paragraph from './components/nodes/paragraph/Paragraph';
|
import ParagraphElement from './components/nodes/paragraph/ParagraphElement';
|
||||||
import { TableCellElement, TableElement, TableRowElement } from './components/nodes/table';
|
import { TableCellElement, TableElement, TableRowElement } from './components/nodes/table';
|
||||||
import { Toolbar } from './components/toolbar';
|
import { Toolbar } from './components/toolbar';
|
||||||
import editableProps from './editableProps';
|
import editableProps from './editableProps';
|
||||||
import { createMdPlugins } from './plateTypes';
|
import { createMdPlugins, ELEMENT_SHORTCODE } from './plateTypes';
|
||||||
import { alignPlugin } from './plugins/align';
|
import { alignPlugin } from './plugins/align';
|
||||||
import { autoformatPlugin } from './plugins/autoformat';
|
import { autoformatPlugin } from './plugins/autoformat';
|
||||||
import { createCodeBlockPlugin } from './plugins/code-block';
|
import { createCodeBlockPlugin } from './plugins/code-block';
|
||||||
@ -87,12 +88,18 @@ import { CursorOverlayContainer } from './plugins/cursor-overlay';
|
|||||||
import { exitBreakPlugin } from './plugins/exit-break';
|
import { exitBreakPlugin } from './plugins/exit-break';
|
||||||
import { createListPlugin } from './plugins/list';
|
import { createListPlugin } from './plugins/list';
|
||||||
import { resetBlockTypePlugin } from './plugins/reset-node';
|
import { resetBlockTypePlugin } from './plugins/reset-node';
|
||||||
|
import { createShortcodePlugin } from './plugins/shortcode';
|
||||||
import { softBreakPlugin } from './plugins/soft-break';
|
import { softBreakPlugin } from './plugins/soft-break';
|
||||||
import { createTablePlugin } from './plugins/table';
|
import { createTablePlugin } from './plugins/table';
|
||||||
import { trailingBlockPlugin } from './plugins/trailing-block';
|
import { trailingBlockPlugin } from './plugins/trailing-block';
|
||||||
|
|
||||||
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
import type {
|
||||||
import type { AutoformatPlugin } from '@udecode/plate';
|
Collection,
|
||||||
|
Entry,
|
||||||
|
MarkdownField,
|
||||||
|
WidgetControlProps,
|
||||||
|
} from '@staticcms/core/interface';
|
||||||
|
import type { AnyObject, AutoformatPlugin, PlatePlugin } from '@udecode/plate';
|
||||||
import type { CSSProperties, FC } from 'react';
|
import type { CSSProperties, FC } from 'react';
|
||||||
import type { MdEditor, MdValue } from './plateTypes';
|
import type { MdEditor, MdValue } from './plateTypes';
|
||||||
|
|
||||||
@ -112,6 +119,8 @@ export interface PlateEditorProps {
|
|||||||
collection: Collection<MarkdownField>;
|
collection: Collection<MarkdownField>;
|
||||||
entry: Entry;
|
entry: Entry;
|
||||||
field: MarkdownField;
|
field: MarkdownField;
|
||||||
|
useMdx: boolean;
|
||||||
|
controlProps: WidgetControlProps<string, MarkdownField>;
|
||||||
onChange: (value: MdValue) => void;
|
onChange: (value: MdValue) => void;
|
||||||
onFocus: () => void;
|
onFocus: () => void;
|
||||||
onBlur: () => void;
|
onBlur: () => void;
|
||||||
@ -122,6 +131,8 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
collection,
|
collection,
|
||||||
entry,
|
entry,
|
||||||
field,
|
field,
|
||||||
|
useMdx,
|
||||||
|
controlProps,
|
||||||
onChange,
|
onChange,
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
@ -130,15 +141,15 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
const editorContainerRef = useRef<HTMLDivElement | null>(null);
|
const editorContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const innerEditorContainerRef = useRef<HTMLDivElement | null>(null);
|
const innerEditorContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const components = useMemo(
|
const components = useMemo(() => {
|
||||||
() => ({
|
const baseComponents = {
|
||||||
[ELEMENT_H1]: Heading1,
|
[ELEMENT_H1]: Heading1,
|
||||||
[ELEMENT_H2]: Heading2,
|
[ELEMENT_H2]: Heading2,
|
||||||
[ELEMENT_H3]: Heading3,
|
[ELEMENT_H3]: Heading3,
|
||||||
[ELEMENT_H4]: Heading4,
|
[ELEMENT_H4]: Heading4,
|
||||||
[ELEMENT_H5]: Heading5,
|
[ELEMENT_H5]: Heading5,
|
||||||
[ELEMENT_H6]: Heading6,
|
[ELEMENT_H6]: Heading6,
|
||||||
[ELEMENT_PARAGRAPH]: Paragraph,
|
[ELEMENT_PARAGRAPH]: ParagraphElement,
|
||||||
[ELEMENT_TABLE]: TableElement,
|
[ELEMENT_TABLE]: TableElement,
|
||||||
[ELEMENT_TR]: TableRowElement,
|
[ELEMENT_TR]: TableRowElement,
|
||||||
[ELEMENT_TH]: TableCellElement,
|
[ELEMENT_TH]: TableCellElement,
|
||||||
@ -161,32 +172,31 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
[ELEMENT_UL]: UnorderedListElement,
|
[ELEMENT_UL]: UnorderedListElement,
|
||||||
[ELEMENT_LI]: ListItemElement,
|
[ELEMENT_LI]: ListItemElement,
|
||||||
[ELEMENT_LIC]: ListItemContentElement,
|
[ELEMENT_LIC]: ListItemContentElement,
|
||||||
|
[ELEMENT_SHORTCODE]: withShortcodeElement({ controlProps }),
|
||||||
[MARK_BOLD]: withProps(StyledLeaf, { as: 'strong' }),
|
[MARK_BOLD]: withProps(StyledLeaf, { as: 'strong' }),
|
||||||
[MARK_ITALIC]: withProps(StyledLeaf, { as: 'em' }),
|
[MARK_ITALIC]: withProps(StyledLeaf, { as: 'em' }),
|
||||||
[MARK_STRIKETHROUGH]: withProps(StyledLeaf, { as: 's' }),
|
[MARK_STRIKETHROUGH]: withProps(StyledLeaf, { as: 's' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (useMdx) {
|
||||||
|
// MDX Widget
|
||||||
|
return {
|
||||||
|
...baseComponents,
|
||||||
[MARK_SUBSCRIPT]: withProps(StyledLeaf, { as: 'sub' }),
|
[MARK_SUBSCRIPT]: withProps(StyledLeaf, { as: 'sub' }),
|
||||||
[MARK_SUPERSCRIPT]: withProps(StyledLeaf, { as: 'sup' }),
|
[MARK_SUPERSCRIPT]: withProps(StyledLeaf, { as: 'sup' }),
|
||||||
[MARK_UNDERLINE]: withProps(StyledLeaf, { as: 'u' }),
|
[MARK_UNDERLINE]: withProps(StyledLeaf, { as: 'u' }),
|
||||||
}),
|
};
|
||||||
[collection, entry, field],
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const [hasEditorFocus, setHasEditorFocus] = useState(false);
|
// Markdown widget
|
||||||
|
return {
|
||||||
|
...baseComponents,
|
||||||
|
[ELEMENT_SHORTCODE]: withShortcodeElement({ controlProps }),
|
||||||
|
};
|
||||||
|
}, [collection, controlProps, entry, field, useMdx]);
|
||||||
|
|
||||||
const handleOnFocus = useCallback(() => {
|
const plugins = useMemo(() => {
|
||||||
setHasEditorFocus(true);
|
const basePlugins: PlatePlugin<AnyObject, MdValue>[] = [
|
||||||
onFocus();
|
|
||||||
}, [onFocus]);
|
|
||||||
|
|
||||||
const handleOnBlur = useCallback(() => {
|
|
||||||
setHasEditorFocus(false);
|
|
||||||
onBlur();
|
|
||||||
}, [onBlur]);
|
|
||||||
|
|
||||||
const plugins = useMemo(
|
|
||||||
() =>
|
|
||||||
createMdPlugins(
|
|
||||||
[
|
|
||||||
createParagraphPlugin(),
|
createParagraphPlugin(),
|
||||||
createBlockquotePlugin(),
|
createBlockquotePlugin(),
|
||||||
createTodoListPlugin(),
|
createTodoListPlugin(),
|
||||||
@ -198,17 +208,11 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
createTablePlugin(),
|
createTablePlugin(),
|
||||||
// createMediaEmbedPlugin(),
|
// createMediaEmbedPlugin(),
|
||||||
createCodeBlockPlugin(),
|
createCodeBlockPlugin(),
|
||||||
createAlignPlugin(alignPlugin),
|
|
||||||
createBoldPlugin(),
|
createBoldPlugin(),
|
||||||
createCodePlugin(),
|
createCodePlugin(),
|
||||||
createItalicPlugin(),
|
createItalicPlugin(),
|
||||||
// createHighlightPlugin(),
|
// createHighlightPlugin(),
|
||||||
createUnderlinePlugin(),
|
|
||||||
createStrikethroughPlugin(),
|
createStrikethroughPlugin(),
|
||||||
createSubscriptPlugin(),
|
|
||||||
createSuperscriptPlugin(),
|
|
||||||
createFontColorPlugin(),
|
|
||||||
createFontBackgroundColorPlugin(),
|
|
||||||
// createFontSizePlugin(),
|
// createFontSizePlugin(),
|
||||||
// createKbdPlugin(),
|
// createKbdPlugin(),
|
||||||
// createNodeIdPlugin(),
|
// createNodeIdPlugin(),
|
||||||
@ -229,13 +233,31 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
// createDeserializeCsvPlugin(),
|
// createDeserializeCsvPlugin(),
|
||||||
// createDeserializeDocxPlugin(),
|
// createDeserializeDocxPlugin(),
|
||||||
// createJuicePlugin() as MdPlatePlugin,
|
// createJuicePlugin() as MdPlatePlugin,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (useMdx) {
|
||||||
|
// MDX Widget
|
||||||
|
return createMdPlugins(
|
||||||
|
[
|
||||||
|
...basePlugins,
|
||||||
|
createFontColorPlugin(),
|
||||||
|
createFontBackgroundColorPlugin(),
|
||||||
|
createSubscriptPlugin(),
|
||||||
|
createSuperscriptPlugin(),
|
||||||
|
createUnderlinePlugin(),
|
||||||
|
createAlignPlugin(alignPlugin),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
components,
|
components,
|
||||||
},
|
},
|
||||||
),
|
|
||||||
[components],
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown Widget
|
||||||
|
return createMdPlugins([...basePlugins, createShortcodePlugin()], {
|
||||||
|
components,
|
||||||
|
});
|
||||||
|
}, [components, useMdx]);
|
||||||
|
|
||||||
const id = useUUID();
|
const id = useUUID();
|
||||||
|
|
||||||
@ -253,6 +275,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
<div key="editor-outer_wrapper" ref={outerEditorContainerRef} style={styles.container}>
|
<div key="editor-outer_wrapper" ref={outerEditorContainerRef} style={styles.container}>
|
||||||
<Toolbar
|
<Toolbar
|
||||||
key="toolbar"
|
key="toolbar"
|
||||||
|
useMdx={useMdx}
|
||||||
containerRef={outerEditorContainerRef.current}
|
containerRef={outerEditorContainerRef.current}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
field={field}
|
field={field}
|
||||||
@ -265,8 +288,8 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
id={id}
|
id={id}
|
||||||
editableProps={{
|
editableProps={{
|
||||||
...editableProps,
|
...editableProps,
|
||||||
onFocus: handleOnFocus,
|
onFocus,
|
||||||
onBlur: handleOnBlur,
|
onBlur,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -276,8 +299,8 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
>
|
>
|
||||||
<BalloonToolbar
|
<BalloonToolbar
|
||||||
key="balloon-toolbar"
|
key="balloon-toolbar"
|
||||||
|
useMdx={useMdx}
|
||||||
containerRef={innerEditorContainerRef.current}
|
containerRef={innerEditorContainerRef.current}
|
||||||
hasEditorFocus={hasEditorFocus}
|
|
||||||
collection={collection}
|
collection={collection}
|
||||||
field={field}
|
field={field}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
@ -292,7 +315,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
</StyledPlateEditor>
|
</StyledPlateEditor>
|
||||||
),
|
),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[collection, field, handleOnBlur, handleOnFocus, initialValue, onChange, plugins],
|
[collection, field, onBlur, onFocus, initialValue, onChange, plugins],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
someNode,
|
someNode,
|
||||||
usePlateSelection,
|
usePlateSelection,
|
||||||
} from '@udecode/plate';
|
} from '@udecode/plate';
|
||||||
|
import { useFocused } from 'slate-react';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||||
@ -25,6 +26,7 @@ import BasicElementToolbarButtons from '../buttons/BasicElementToolbarButtons';
|
|||||||
import BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
|
import BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
|
||||||
import MediaToolbarButtons from '../buttons/MediaToolbarButtons';
|
import MediaToolbarButtons from '../buttons/MediaToolbarButtons';
|
||||||
import TableToolbarButtons from '../buttons/TableToolbarButtons';
|
import TableToolbarButtons from '../buttons/TableToolbarButtons';
|
||||||
|
import ShortcodeToolbarButton from '../buttons/ShortcodeToolbarButton';
|
||||||
|
|
||||||
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
||||||
import type { ClientRectObject } from '@udecode/plate';
|
import type { ClientRectObject } from '@udecode/plate';
|
||||||
@ -54,20 +56,21 @@ const StyledDivider = styled('div')(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export interface BalloonToolbarProps {
|
export interface BalloonToolbarProps {
|
||||||
|
useMdx: boolean;
|
||||||
containerRef: HTMLElement | null;
|
containerRef: HTMLElement | null;
|
||||||
hasEditorFocus: boolean;
|
|
||||||
collection: Collection<MarkdownField>;
|
collection: Collection<MarkdownField>;
|
||||||
field: MarkdownField;
|
field: MarkdownField;
|
||||||
entry: Entry;
|
entry: Entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
||||||
|
useMdx,
|
||||||
containerRef,
|
containerRef,
|
||||||
hasEditorFocus,
|
|
||||||
collection,
|
collection,
|
||||||
field,
|
field,
|
||||||
entry,
|
entry,
|
||||||
}) => {
|
}) => {
|
||||||
|
const hasEditorFocus = useFocused();
|
||||||
const editor = useMdPlateEditorState();
|
const editor = useMdPlateEditorState();
|
||||||
const selection = usePlateSelection();
|
const selection = usePlateSelection();
|
||||||
const [hasFocus, setHasFocus] = useState(false);
|
const [hasFocus, setHasFocus] = useState(false);
|
||||||
@ -126,9 +129,10 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Selected text buttons
|
||||||
if (selectionText && selectionExpanded) {
|
if (selectionText && selectionExpanded) {
|
||||||
return [
|
return [
|
||||||
<BasicMarkToolbarButtons key="selection-basic-mark-buttons" />,
|
<BasicMarkToolbarButtons key="selection-basic-mark-buttons" useMdx={useMdx} />,
|
||||||
<BasicElementToolbarButtons
|
<BasicElementToolbarButtons
|
||||||
key="selection-basic-element-buttons"
|
key="selection-basic-element-buttons"
|
||||||
hideFontTypeSelect={isInTableCell}
|
hideFontTypeSelect={isInTableCell}
|
||||||
@ -147,6 +151,7 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Empty paragraph, not first line
|
||||||
if (
|
if (
|
||||||
editor.children.length > 1 &&
|
editor.children.length > 1 &&
|
||||||
node &&
|
node &&
|
||||||
@ -164,7 +169,7 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
parent[0].children.length === 1
|
parent[0].children.length === 1
|
||||||
) {
|
) {
|
||||||
return [
|
return [
|
||||||
<BasicMarkToolbarButtons key="empty-basic-mark-buttons" />,
|
<BasicMarkToolbarButtons key="empty-basic-mark-buttons" useMdx={useMdx} />,
|
||||||
<BasicElementToolbarButtons
|
<BasicElementToolbarButtons
|
||||||
key="empty-basic-element-buttons"
|
key="empty-basic-element-buttons"
|
||||||
hideFontTypeSelect={isInTableCell}
|
hideFontTypeSelect={isInTableCell}
|
||||||
@ -179,6 +184,7 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
entry={entry}
|
entry={entry}
|
||||||
onMediaToggle={setMediaOpen}
|
onMediaToggle={setMediaOpen}
|
||||||
/>,
|
/>,
|
||||||
|
!useMdx ? <ShortcodeToolbarButton key="shortcode-button" /> : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,18 +192,20 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
return [];
|
return [];
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
collection,
|
mediaOpen,
|
||||||
editor,
|
debouncedEditorFocus,
|
||||||
field,
|
|
||||||
hasFocus,
|
hasFocus,
|
||||||
debouncedHasFocus,
|
debouncedHasFocus,
|
||||||
debouncedEditorFocus,
|
|
||||||
isInTableCell,
|
|
||||||
mediaOpen,
|
|
||||||
node,
|
|
||||||
selection,
|
selection,
|
||||||
selectionExpanded,
|
editor,
|
||||||
selectionText,
|
selectionText,
|
||||||
|
selectionExpanded,
|
||||||
|
node,
|
||||||
|
useMdx,
|
||||||
|
isInTableCell,
|
||||||
|
containerRef,
|
||||||
|
collection,
|
||||||
|
field,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [prevSelectionBoundingClientRect, setPrevSelectionBoundingClientRect] = useState(
|
const [prevSelectionBoundingClientRect, setPrevSelectionBoundingClientRect] = useState(
|
||||||
@ -243,7 +251,7 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
>
|
>
|
||||||
<StyledPopperContent>
|
<StyledPopperContent>
|
||||||
{(groups.length > 0 ? groups : debouncedGroups).map((group, index) => [
|
{(groups.length > 0 ? groups : debouncedGroups).map((group, index) => [
|
||||||
index !== 0 ? <StyledDivider key={`table-divider-${index}`} /> : null,
|
index !== 0 ? <StyledDivider key={`balloon-toolbar-divider-${index}`} /> : null,
|
||||||
group,
|
group,
|
||||||
])}
|
])}
|
||||||
</StyledPopperContent>
|
</StyledPopperContent>
|
||||||
|
@ -10,9 +10,24 @@ import type { FC } from 'react';
|
|||||||
const AlignToolbarButtons: FC = () => {
|
const AlignToolbarButtons: FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AlignToolbarButton tooltip="Align Left" value="left" icon={<FormatAlignLeftIcon />} />
|
<AlignToolbarButton
|
||||||
<AlignToolbarButton tooltip="Align Center" value="center" icon={<FormatAlignCenterIcon />} />
|
key="algin-button-left"
|
||||||
<AlignToolbarButton tooltip="Align Right" value="right" icon={<FormatAlignRightIcon />} />
|
tooltip="Align Left"
|
||||||
|
value="left"
|
||||||
|
icon={<FormatAlignLeftIcon />}
|
||||||
|
/>
|
||||||
|
<AlignToolbarButton
|
||||||
|
key="algin-button-center"
|
||||||
|
tooltip="Align Center"
|
||||||
|
value="center"
|
||||||
|
icon={<FormatAlignCenterIcon />}
|
||||||
|
/>
|
||||||
|
<AlignToolbarButton
|
||||||
|
key="algin-button-right"
|
||||||
|
tooltip="Align Right"
|
||||||
|
value="right"
|
||||||
|
icon={<FormatAlignRightIcon />}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -22,33 +22,42 @@ import type { FC } from 'react';
|
|||||||
|
|
||||||
export interface BasicMarkToolbarButtonsProps {
|
export interface BasicMarkToolbarButtonsProps {
|
||||||
extended?: boolean;
|
extended?: boolean;
|
||||||
|
useMdx: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BasicMarkToolbarButtons: FC<BasicMarkToolbarButtonsProps> = ({ extended = false }) => {
|
const BasicMarkToolbarButtons: FC<BasicMarkToolbarButtonsProps> = ({
|
||||||
|
extended = false,
|
||||||
|
useMdx,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MarkToolbarButton tooltip="Bold" type={MARK_BOLD} icon={<FormatBoldIcon />} />
|
<MarkToolbarButton tooltip="Bold" type={MARK_BOLD} icon={<FormatBoldIcon />} />
|
||||||
<MarkToolbarButton tooltip="Italic" type={MARK_ITALIC} icon={<FormatItalicIcon />} />
|
<MarkToolbarButton tooltip="Italic" type={MARK_ITALIC} icon={<FormatItalicIcon />} />
|
||||||
|
{useMdx ? (
|
||||||
<MarkToolbarButton
|
<MarkToolbarButton
|
||||||
|
key="underline-button"
|
||||||
tooltip="Underline"
|
tooltip="Underline"
|
||||||
type={MARK_UNDERLINE}
|
type={MARK_UNDERLINE}
|
||||||
icon={<FormatUnderlinedIcon />}
|
icon={<FormatUnderlinedIcon />}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
<MarkToolbarButton
|
<MarkToolbarButton
|
||||||
tooltip="Strikethrough"
|
tooltip="Strikethrough"
|
||||||
type={MARK_STRIKETHROUGH}
|
type={MARK_STRIKETHROUGH}
|
||||||
icon={<FormatStrikethroughIcon />}
|
icon={<FormatStrikethroughIcon />}
|
||||||
/>
|
/>
|
||||||
<MarkToolbarButton tooltip="Code" type={MARK_CODE} icon={<CodeIcon />} />
|
<MarkToolbarButton tooltip="Code" type={MARK_CODE} icon={<CodeIcon />} />
|
||||||
{extended ? (
|
{useMdx && extended ? (
|
||||||
<>
|
<>
|
||||||
<MarkToolbarButton
|
<MarkToolbarButton
|
||||||
|
key="superscript-button"
|
||||||
tooltip="Superscript"
|
tooltip="Superscript"
|
||||||
type={MARK_SUPERSCRIPT}
|
type={MARK_SUPERSCRIPT}
|
||||||
clear={MARK_SUBSCRIPT}
|
clear={MARK_SUBSCRIPT}
|
||||||
icon={<SuperscriptIcon />}
|
icon={<SuperscriptIcon />}
|
||||||
/>
|
/>
|
||||||
<MarkToolbarButton
|
<MarkToolbarButton
|
||||||
|
key="subscript-button"
|
||||||
tooltip="Subscript"
|
tooltip="Subscript"
|
||||||
type={MARK_SUBSCRIPT}
|
type={MARK_SUBSCRIPT}
|
||||||
clear={MARK_SUPERSCRIPT}
|
clear={MARK_SUPERSCRIPT}
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
import DataArrayIcon from '@mui/icons-material/DataArray';
|
||||||
|
import Menu from '@mui/material/Menu';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import { focusEditor, insertNodes } from '@udecode/plate-core';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { getShortcodes } from '../../../../../lib/registry';
|
||||||
|
import { toTitleCase } from '../../../../../lib/util/string.util';
|
||||||
|
import { ELEMENT_SHORTCODE, useMdPlateEditorState } from '../../plateTypes';
|
||||||
|
import ToolbarButton from './common/ToolbarButton';
|
||||||
|
|
||||||
|
import type { FC, MouseEvent } from 'react';
|
||||||
|
import type { MdEditor } from '../../plateTypes';
|
||||||
|
|
||||||
|
const ShortcodeToolbarButton: FC = () => {
|
||||||
|
const editor = useMdPlateEditorState();
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const handleClick = useCallback((_editor: MdEditor, event: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const configs = useMemo(() => getShortcodes(), []);
|
||||||
|
|
||||||
|
const handleShortcodeClick = useCallback(
|
||||||
|
(shortcode: string) => () => {
|
||||||
|
insertNodes(editor, {
|
||||||
|
type: ELEMENT_SHORTCODE,
|
||||||
|
shortcode,
|
||||||
|
args: [],
|
||||||
|
children: [{ text: '' }],
|
||||||
|
});
|
||||||
|
focusEditor(editor);
|
||||||
|
handleClose();
|
||||||
|
},
|
||||||
|
[editor, handleClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToolbarButton
|
||||||
|
key="shortcode-button"
|
||||||
|
tooltip="Add Shortcode"
|
||||||
|
icon={<DataArrayIcon />}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
id="shortcode-menu"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
MenuListProps={{
|
||||||
|
'aria-labelledby': 'shortcode-button',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.keys(configs).map(name => {
|
||||||
|
const config = configs[name];
|
||||||
|
return (
|
||||||
|
<MenuItem key={`shortcode-${name}`} onClick={handleShortcodeClick(name)}>
|
||||||
|
{config.label ?? toTitleCase(name)}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShortcodeToolbarButton;
|
@ -154,6 +154,7 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
anchorEl &&
|
||||||
!debouncedHasEditorFocus &&
|
!debouncedHasEditorFocus &&
|
||||||
!hasEditorFocus &&
|
!hasEditorFocus &&
|
||||||
!hasFocus &&
|
!hasFocus &&
|
||||||
@ -163,6 +164,7 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
|||||||
handleClose(false);
|
handleClose(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
anchorEl,
|
||||||
debouncedHasEditorFocus,
|
debouncedHasEditorFocus,
|
||||||
debouncedHasFocus,
|
debouncedHasFocus,
|
||||||
handleClose,
|
handleClose,
|
||||||
|
@ -117,7 +117,7 @@ const withImageElement = ({ containerRef, collection, entry, field }: WithImageE
|
|||||||
}, [handleClose, hasEditorFocus, element, selection, editor, handleOpenPopover]);
|
}, [handleClose, hasEditorFocus, element, selection, editor, handleOpenPopover]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onBlur={handleBlur}>
|
<span onBlur={handleBlur}>
|
||||||
<img
|
<img
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
src={assetSource}
|
src={assetSource}
|
||||||
@ -142,7 +142,7 @@ const withImageElement = ({ containerRef, collection, entry, field }: WithImageE
|
|||||||
forImage
|
forImage
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,4 +7,5 @@ export * from './image';
|
|||||||
export * from './link';
|
export * from './link';
|
||||||
export * from './list';
|
export * from './list';
|
||||||
export * from './paragraph';
|
export * from './paragraph';
|
||||||
|
export * from './shortcode';
|
||||||
export * from './table';
|
export * from './table';
|
||||||
|
@ -4,11 +4,11 @@ import type { MdParagraphElement, MdValue } from '@staticcms/markdown';
|
|||||||
import type { PlateRenderElementProps } from '@udecode/plate';
|
import type { PlateRenderElementProps } from '@udecode/plate';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
const Paragraph: FC<PlateRenderElementProps<MdValue, MdParagraphElement>> = ({
|
const ParagraphElement: FC<PlateRenderElementProps<MdValue, MdParagraphElement>> = ({
|
||||||
children,
|
children,
|
||||||
element: { align },
|
element: { align },
|
||||||
}) => {
|
}) => {
|
||||||
return <p style={{ textAlign: align }}>{children}</p>;
|
return <p style={{ textAlign: align }}>{children}</p>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Paragraph;
|
export default ParagraphElement;
|
@ -1,2 +1,2 @@
|
|||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
export { default as Paragraph } from './Paragraph';
|
export { default as Paragraph } from './ParagraphElement';
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './withShortcodeElement';
|
||||||
|
export { default as withShortcodeElement } from './withShortcodeElement';
|
@ -0,0 +1,57 @@
|
|||||||
|
import { findNodePath, setNodes } from '@udecode/plate';
|
||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { getShortcode } from '../../../../../../lib/registry';
|
||||||
|
|
||||||
|
import type { MarkdownField, WidgetControlProps } from '@staticcms/core/interface';
|
||||||
|
import type { MdShortcodeElement, MdValue } from '@staticcms/markdown';
|
||||||
|
import type { PlateRenderElementProps } from '@udecode/plate';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
export interface WithShortcodeElementProps {
|
||||||
|
controlProps: WidgetControlProps<string, MarkdownField>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withShortcodeElement = ({ controlProps }: WithShortcodeElementProps) => {
|
||||||
|
const ShortcodeElement: FC<PlateRenderElementProps<MdValue, MdShortcodeElement>> = ({
|
||||||
|
element,
|
||||||
|
editor,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const config = useMemo(() => getShortcode(element.shortcode), [element.shortcode]);
|
||||||
|
|
||||||
|
const [ShortcodeControl, props] = useMemo(() => {
|
||||||
|
if (!config) {
|
||||||
|
return [null, {}];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = config.toProps ? config.toProps(element.args) : {};
|
||||||
|
return [config.control, props];
|
||||||
|
}, [config, element.args]);
|
||||||
|
|
||||||
|
const handleOnChange = useCallback(
|
||||||
|
(props: {}) => {
|
||||||
|
if (!config || !config.toArgs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = findNodePath(editor, element);
|
||||||
|
path && setNodes(editor, { args: config.toArgs(props) }, { at: path });
|
||||||
|
},
|
||||||
|
[config, editor, element],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span contentEditable={false}>
|
||||||
|
{ShortcodeControl ? (
|
||||||
|
<ShortcodeControl controlProps={controlProps} onChange={handleOnChange} {...props} />
|
||||||
|
) : null}
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return ShortcodeElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withShortcodeElement;
|
@ -7,6 +7,7 @@ import BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
|
|||||||
import ColorToolbarButtons from '../buttons/ColorToolbarButtons';
|
import ColorToolbarButtons from '../buttons/ColorToolbarButtons';
|
||||||
import ListToolbarButtons from '../buttons/ListToolbarButtons';
|
import ListToolbarButtons from '../buttons/ListToolbarButtons';
|
||||||
import MediaToolbarButton from '../buttons/MediaToolbarButtons';
|
import MediaToolbarButton from '../buttons/MediaToolbarButtons';
|
||||||
|
import ShortcodeToolbarButton from '../buttons/ShortcodeToolbarButton';
|
||||||
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
||||||
@ -42,32 +43,36 @@ const StyledDivider = styled('div')(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export interface ToolbarProps {
|
export interface ToolbarProps {
|
||||||
|
useMdx: boolean;
|
||||||
containerRef: HTMLElement | null;
|
containerRef: HTMLElement | null;
|
||||||
collection: Collection<MarkdownField>;
|
collection: Collection<MarkdownField>;
|
||||||
field: MarkdownField;
|
field: MarkdownField;
|
||||||
entry: Entry;
|
entry: Entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Toolbar: FC<ToolbarProps> = ({ containerRef, collection, field, entry }) => {
|
const Toolbar: FC<ToolbarProps> = ({ useMdx, containerRef, collection, field, entry }) => {
|
||||||
return (
|
const groups = [
|
||||||
<StyledToolbar>
|
<BasicMarkToolbarButtons key="basic-mark-buttons" useMdx={useMdx} extended />,
|
||||||
<BasicMarkToolbarButtons key="basic-mark-buttons" extended />
|
<BasicElementToolbarButtons key="basic-element-buttons" />,
|
||||||
<StyledDivider />
|
<ListToolbarButtons key="list-buttons" />,
|
||||||
<BasicElementToolbarButtons key="basic-element-buttons" />
|
useMdx ? <ColorToolbarButtons key="color-buttons" /> : null,
|
||||||
<StyledDivider />
|
useMdx ? <AlignToolbarButtons key="align-mark-buttons" /> : null,
|
||||||
<ListToolbarButtons key="list-buttons" />
|
|
||||||
<StyledDivider />
|
|
||||||
<ColorToolbarButtons key="color-buttons" />
|
|
||||||
<StyledDivider />
|
|
||||||
<AlignToolbarButtons key="align-mark-buttons" />
|
|
||||||
<StyledDivider />
|
|
||||||
<MediaToolbarButton
|
<MediaToolbarButton
|
||||||
key="media-buttons"
|
key="media-buttons"
|
||||||
containerRef={containerRef}
|
containerRef={containerRef}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
field={field}
|
field={field}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
/>
|
/>,
|
||||||
|
!useMdx ? <ShortcodeToolbarButton key="shortcode-button" /> : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledToolbar>
|
||||||
|
{groups.map((group, index) => [
|
||||||
|
index !== 0 ? <StyledDivider key={`toolbar-divider-${index}`} /> : null,
|
||||||
|
group,
|
||||||
|
])}
|
||||||
</StyledToolbar>
|
</StyledToolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
deserializationOnlyTestData,
|
||||||
|
runSerializationTests,
|
||||||
|
testShortcodeConfigs as shortcodeConfigs,
|
||||||
|
} from '../../tests-util/serializationTests.util';
|
||||||
|
import { markdownToSlate } from '../useMarkdownToSlate';
|
||||||
|
|
||||||
|
import type { SerializationTestData } from '../../tests-util/serializationTests.util';
|
||||||
|
import type { UseMarkdownToSlateOptions } from '../useMarkdownToSlate';
|
||||||
|
|
||||||
|
async function expectNodes(
|
||||||
|
markdown: string,
|
||||||
|
options: UseMarkdownToSlateOptions,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
children: any[],
|
||||||
|
) {
|
||||||
|
expect(await markdownToSlate(markdown, options)).toEqual(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeHtmlInMarkdown(markdown: string) {
|
||||||
|
return markdown
|
||||||
|
.replace('</font>', '<\\/font>')
|
||||||
|
.replace('<u>', '<u\\>')
|
||||||
|
.replace('</u>', '<\\/u>')
|
||||||
|
.replace('<sub>', '<sub\\>')
|
||||||
|
.replace('</sub>', '<\\/sub>')
|
||||||
|
.replace('<sup>', '<sup\\>')
|
||||||
|
.replace('</sup>', '<\\/sup>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testRunner(key: string, mode: 'markdown' | 'mdx' | 'both', data: SerializationTestData) {
|
||||||
|
it(`deserializes ${key}`, async () => {
|
||||||
|
if (mode === 'both') {
|
||||||
|
await expectNodes(data.markdown, { shortcodeConfigs, useMdx: false }, data.slate);
|
||||||
|
await expectNodes(data.markdown, { shortcodeConfigs, useMdx: true }, data.slate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await expectNodes(
|
||||||
|
mode === 'markdown' ? sanitizeHtmlInMarkdown(data.markdown) : data.markdown,
|
||||||
|
{ shortcodeConfigs, useMdx: mode === 'mdx' },
|
||||||
|
data.slate,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('markdownToSlate', () => {
|
||||||
|
runSerializationTests(testRunner);
|
||||||
|
runSerializationTests(testRunner, deserializationOnlyTestData);
|
||||||
|
});
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from './useMarkdownToSlate';
|
||||||
export { default as useMarkdownToSlate } from './useMarkdownToSlate';
|
export { default as useMarkdownToSlate } from './useMarkdownToSlate';
|
||||||
export * from './useMdx';
|
export * from './useMdx';
|
||||||
export { default as useMdx } from './useMdx';
|
export { default as useMdx } from './useMdx';
|
||||||
|
61
core/src/widgets/markdown/plate/hooks/useMarkdownToSlate.ts
Normal file
61
core/src/widgets/markdown/plate/hooks/useMarkdownToSlate.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { ELEMENT_PARAGRAPH } from '@udecode/plate';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import gfm from 'remark-gfm';
|
||||||
|
import mdx from 'remark-mdx';
|
||||||
|
import markdown from 'remark-parse';
|
||||||
|
import { unified } from 'unified';
|
||||||
|
|
||||||
|
import { getShortcodes } from '../../../../lib/registry';
|
||||||
|
import toSlatePlugin from '../serialization/slate/toSlatePlugin';
|
||||||
|
|
||||||
|
import type { ShortcodeConfig } from '../../../../interface';
|
||||||
|
import type { MdValue } from '../plateTypes';
|
||||||
|
|
||||||
|
export interface UseMarkdownToSlateOptions {
|
||||||
|
shortcodeConfigs?: Record<string, ShortcodeConfig>;
|
||||||
|
useMdx: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const markdownToSlate = async (
|
||||||
|
markdownValue: string,
|
||||||
|
{ useMdx, shortcodeConfigs }: UseMarkdownToSlateOptions,
|
||||||
|
) => {
|
||||||
|
return new Promise<MdValue>(resolve => {
|
||||||
|
unified()
|
||||||
|
.use(markdown)
|
||||||
|
.use(gfm)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
.use(useMdx ? mdx : () => {})
|
||||||
|
.use(toSlatePlugin({ shortcodeConfigs: shortcodeConfigs ?? getShortcodes(), useMdx }))
|
||||||
|
.process(markdownValue, (err, file) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(file?.result as MdValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const useMarkdownToSlate = (
|
||||||
|
markdownValue: string,
|
||||||
|
options: UseMarkdownToSlateOptions,
|
||||||
|
): [MdValue, boolean] => {
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
const [slateValue, setSlateValue] = useState<MdValue>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
markdownToSlate(markdownValue, options).then(value => {
|
||||||
|
setSlateValue(value);
|
||||||
|
setLoaded(true);
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [
|
||||||
|
slateValue.length > 0 ? slateValue : [{ type: ELEMENT_PARAGRAPH, children: [{ text: '' }] }],
|
||||||
|
loaded,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useMarkdownToSlate;
|
@ -1,40 +0,0 @@
|
|||||||
import { ELEMENT_PARAGRAPH } from '@udecode/plate';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import gfm from 'remark-gfm';
|
|
||||||
import mdx from 'remark-mdx';
|
|
||||||
import markdown from 'remark-parse';
|
|
||||||
import { unified } from 'unified';
|
|
||||||
|
|
||||||
import toSlatePlugin from '../serialization/slate/toSlatePlugin';
|
|
||||||
|
|
||||||
import type { MdValue } from '../plateTypes';
|
|
||||||
|
|
||||||
const useMarkdownToSlate = (markdownValue: string): [MdValue, boolean] => {
|
|
||||||
const [loaded, setLoaded] = useState(false);
|
|
||||||
const [slateValue, setSlateValue] = useState<MdValue>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
unified()
|
|
||||||
.use(markdown)
|
|
||||||
.use(gfm)
|
|
||||||
.use(mdx)
|
|
||||||
.use(toSlatePlugin)
|
|
||||||
.process(markdownValue, (err, file) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSlateValue(file?.result as MdValue);
|
|
||||||
setLoaded(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return [
|
|
||||||
slateValue.length > 0 ? slateValue : [{ type: ELEMENT_PARAGRAPH, children: [{ text: '' }] }],
|
|
||||||
loaded,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useMarkdownToSlate;
|
|
@ -1,4 +1,5 @@
|
|||||||
import { evaluate } from '@mdx-js/mdx';
|
import { evaluate } from '@mdx-js/mdx';
|
||||||
|
import * as provider from '@mdx-js/react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import * as runtime from 'react/jsx-runtime';
|
import * as runtime from 'react/jsx-runtime';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
@ -20,6 +21,7 @@ export default function useMdx(input: string): [UseMdxState, (value: string) =>
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const options: any = {
|
const options: any = {
|
||||||
|
...provider,
|
||||||
...runtime,
|
...runtime,
|
||||||
useDynamicImport: true,
|
useDynamicImport: true,
|
||||||
remarkPlugins: [remarkGfm, flattenListItemParagraphs],
|
remarkPlugins: [remarkGfm, flattenListItemParagraphs],
|
||||||
|
@ -35,6 +35,7 @@ import type {
|
|||||||
ELEMENT_HR,
|
ELEMENT_HR,
|
||||||
ELEMENT_IMAGE,
|
ELEMENT_IMAGE,
|
||||||
ELEMENT_LI,
|
ELEMENT_LI,
|
||||||
|
ELEMENT_LIC,
|
||||||
ELEMENT_LINK,
|
ELEMENT_LINK,
|
||||||
ELEMENT_MEDIA_EMBED,
|
ELEMENT_MEDIA_EMBED,
|
||||||
ELEMENT_MENTION,
|
ELEMENT_MENTION,
|
||||||
@ -79,6 +80,8 @@ import type {
|
|||||||
} from '@udecode/plate';
|
} from '@udecode/plate';
|
||||||
import type { CSSProperties } from 'styled-components';
|
import type { CSSProperties } from 'styled-components';
|
||||||
|
|
||||||
|
export const ELEMENT_SHORTCODE = 'shortcode' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text
|
* Text
|
||||||
*/
|
*/
|
||||||
@ -125,7 +128,12 @@ export interface MdMentionElement extends TMentionElement {
|
|||||||
children: [EmptyText];
|
children: [EmptyText];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MdInlineElement = MdLinkElement | MdMentionElement | MdMentionInputElement;
|
export type MdInlineElement =
|
||||||
|
| MdImageElement
|
||||||
|
| MdLinkElement
|
||||||
|
| MdMentionElement
|
||||||
|
| MdMentionInputElement
|
||||||
|
| MdShortcodeElement;
|
||||||
export type MdInlineDescendant = MdInlineElement | RichText;
|
export type MdInlineDescendant = MdInlineElement | RichText;
|
||||||
export type MdInlineChildren = MdInlineDescendant[];
|
export type MdInlineChildren = MdInlineDescendant[];
|
||||||
|
|
||||||
@ -165,6 +173,13 @@ export interface MdParagraphElement extends MdBlockElement {
|
|||||||
align?: 'left' | 'center' | 'right';
|
align?: 'left' | 'center' | 'right';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MdShortcodeElement extends TElement {
|
||||||
|
type: typeof ELEMENT_SHORTCODE;
|
||||||
|
shortcode: string;
|
||||||
|
args: string[];
|
||||||
|
children: [EmptyText];
|
||||||
|
}
|
||||||
|
|
||||||
export interface MdH1Element extends MdBlockElement {
|
export interface MdH1Element extends MdBlockElement {
|
||||||
type: typeof ELEMENT_H1;
|
type: typeof ELEMENT_H1;
|
||||||
children: MdInlineChildren;
|
children: MdInlineChildren;
|
||||||
@ -202,7 +217,7 @@ export interface MdBlockquoteElement extends MdBlockElement {
|
|||||||
|
|
||||||
export interface MdCodeBlockElement extends MdBlockElement {
|
export interface MdCodeBlockElement extends MdBlockElement {
|
||||||
type: typeof ELEMENT_CODE_BLOCK;
|
type: typeof ELEMENT_CODE_BLOCK;
|
||||||
lang: string | undefined;
|
lang: string | undefined | null;
|
||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,6 +254,12 @@ export interface MdNumberedListElement extends TElement, MdBlockElement {
|
|||||||
export interface MdListItemElement extends TElement, MdBlockElement {
|
export interface MdListItemElement extends TElement, MdBlockElement {
|
||||||
type: typeof ELEMENT_LI;
|
type: typeof ELEMENT_LI;
|
||||||
checked: boolean | null;
|
checked: boolean | null;
|
||||||
|
children: MdListItemContentElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MdListItemContentElement extends TElement, MdBlockElement {
|
||||||
|
type: typeof ELEMENT_LIC;
|
||||||
|
checked: boolean | null;
|
||||||
children: MdInlineChildren;
|
children: MdInlineChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ export * from './indent';
|
|||||||
export * from './list';
|
export * from './list';
|
||||||
export * from './reset-node';
|
export * from './reset-node';
|
||||||
export * from './select-on-backspace';
|
export * from './select-on-backspace';
|
||||||
|
export * from './shortcode';
|
||||||
export * from './soft-break';
|
export * from './soft-break';
|
||||||
export * from './table';
|
export * from './table';
|
||||||
export * from './trailing-block';
|
export * from './trailing-block';
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import { createPluginFactory } from '@udecode/plate';
|
||||||
|
|
||||||
|
import { ELEMENT_SHORTCODE } from '../../plateTypes';
|
||||||
|
|
||||||
|
const createShortcodePlugin = createPluginFactory({
|
||||||
|
key: ELEMENT_SHORTCODE,
|
||||||
|
isElement: true,
|
||||||
|
isInline: true,
|
||||||
|
type: ELEMENT_SHORTCODE,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createShortcodePlugin;
|
@ -0,0 +1,2 @@
|
|||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export { default as createShortcodePlugin } from './createShortcodePlugin';
|
@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
runSerializationTests,
|
||||||
|
testShortcodeConfigs as shortcodeConfigs,
|
||||||
|
} from '../../tests-util/serializationTests.util';
|
||||||
|
import serializeMarkdown from '../serializeMarkdown';
|
||||||
|
|
||||||
|
import type { MdValue } from '../../plateTypes';
|
||||||
|
|
||||||
|
function expectMarkdown(nodes: MdValue, options: { useMdx: boolean }, markdown: string) {
|
||||||
|
const result = serializeMarkdown(nodes, { ...options, shortcodeConfigs });
|
||||||
|
expect(result).toBe(`${markdown}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('serializeMarkdown', () => {
|
||||||
|
runSerializationTests((key, mode, data) => {
|
||||||
|
it(`serializes ${key}`, async () => {
|
||||||
|
if (mode === 'both') {
|
||||||
|
await expectMarkdown(data.slate, { useMdx: false }, data.markdown);
|
||||||
|
await expectMarkdown(data.slate, { useMdx: true }, data.markdown);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await expectMarkdown(data.slate, { useMdx: mode === 'mdx' }, data.markdown);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,2 +1,3 @@
|
|||||||
|
export * from './serializeMarkdown';
|
||||||
|
export { default as serializeMarkdown } from './serializeMarkdown';
|
||||||
export * from './slate';
|
export * from './slate';
|
||||||
export { default as serializerMarkdown } from './serializerMarkdown';
|
|
||||||
|
@ -1,22 +1,24 @@
|
|||||||
/* eslint-disable no-case-declarations */
|
/* eslint-disable no-case-declarations */
|
||||||
// import { BlockType, defaultNodeTypes, LeafType, NodeTypes } from './ast-types';
|
import { getShortcodes } from '../../../../lib/registry';
|
||||||
import escapeHtml from 'escape-html';
|
import { isEmpty } from '../../../../lib/util/string.util';
|
||||||
|
|
||||||
import { LIST_TYPES, NodeTypes } from './slate/ast-types';
|
import { LIST_TYPES, NodeTypes } from './slate/ast-types';
|
||||||
|
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import type { ShortcodeConfig } from '../../../../interface';
|
||||||
import type {
|
import type {
|
||||||
MdCodeBlockElement,
|
MdCodeBlockElement,
|
||||||
MdImageElement,
|
MdImageElement,
|
||||||
MdLinkElement,
|
MdLinkElement,
|
||||||
MdListItemElement,
|
MdListItemElement,
|
||||||
MdParagraphElement,
|
MdParagraphElement,
|
||||||
|
MdShortcodeElement,
|
||||||
|
MdValue,
|
||||||
} from '../plateTypes';
|
} from '../plateTypes';
|
||||||
import type { TableNode, BlockType, LeafType } from './slate/ast-types';
|
import type { BlockType, LeafType, TableNode } from './slate/ast-types';
|
||||||
import type { CSSProperties } from 'react';
|
|
||||||
|
|
||||||
type FontStyles = Pick<CSSProperties, 'color' | 'backgroundColor' | 'textAlign'>;
|
type FontStyles = Pick<CSSProperties, 'color' | 'backgroundColor' | 'textAlign'>;
|
||||||
|
|
||||||
interface MdLeafType extends LeafType {
|
export interface MdLeafType extends LeafType {
|
||||||
superscript?: boolean;
|
superscript?: boolean;
|
||||||
subscript?: boolean;
|
subscript?: boolean;
|
||||||
underline?: boolean;
|
underline?: boolean;
|
||||||
@ -24,39 +26,55 @@ interface MdLeafType extends LeafType {
|
|||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MdBlockType extends Omit<BlockType, 'children'> {
|
export interface MdBlockType extends Omit<BlockType, 'children'> {
|
||||||
children: Array<MdBlockType | MdLeafType>;
|
children: Array<MdBlockType | MdLeafType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Options {
|
interface SerializeMarkdownNodeOptions {
|
||||||
isInTable?: boolean;
|
isInTable?: boolean;
|
||||||
isInCode?: boolean;
|
isInCode?: boolean;
|
||||||
listDepth?: number;
|
listDepth?: number;
|
||||||
blockquoteDepth?: number;
|
blockquoteDepth?: number;
|
||||||
ignoreParagraphNewline?: boolean;
|
ignoreParagraphNewline?: boolean;
|
||||||
|
useMdx: boolean;
|
||||||
|
index: number;
|
||||||
|
shortcodeConfigs: Record<string, ShortcodeConfig>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLeafNode = (node: MdBlockType | MdLeafType): node is MdLeafType => {
|
const isLeafNode = (node: MdBlockType | MdLeafType): node is MdLeafType => {
|
||||||
return typeof (node as MdLeafType).text === 'string';
|
return typeof (node as MdLeafType).text === 'string';
|
||||||
};
|
};
|
||||||
|
|
||||||
const VOID_ELEMENTS: Array<keyof typeof NodeTypes> = ['thematic_break', 'image', 'code_block'];
|
const VOID_ELEMENTS: Array<keyof typeof NodeTypes> = [
|
||||||
|
'thematic_break',
|
||||||
|
'image',
|
||||||
|
'code_block',
|
||||||
|
'shortcode',
|
||||||
|
'tableCell',
|
||||||
|
'tableHeaderCell',
|
||||||
|
];
|
||||||
|
|
||||||
const BREAK_TAG = '<br />';
|
const BREAK_TAG = '<br />';
|
||||||
|
|
||||||
const CODE_ELEMENTS = [NodeTypes.code_block];
|
const CODE_ELEMENTS = [NodeTypes.code_block];
|
||||||
|
|
||||||
export default function serializerMarkdown(chunk: MdBlockType | MdLeafType, opts: Options = {}) {
|
function serializeMarkdownNode(
|
||||||
|
chunk: MdBlockType | MdLeafType,
|
||||||
|
opts: SerializeMarkdownNodeOptions,
|
||||||
|
) {
|
||||||
const {
|
const {
|
||||||
ignoreParagraphNewline = false,
|
ignoreParagraphNewline = false,
|
||||||
listDepth = 0,
|
listDepth = 0,
|
||||||
isInTable = false,
|
isInTable = false,
|
||||||
isInCode = false,
|
isInCode = false,
|
||||||
blockquoteDepth = 0,
|
blockquoteDepth = 0,
|
||||||
|
useMdx,
|
||||||
|
shortcodeConfigs,
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
const text = (chunk as MdLeafType).text || '';
|
const text = (chunk as MdLeafType).text || '';
|
||||||
let type = (chunk as MdBlockType).type || '';
|
let type = (chunk as MdBlockType).type || '';
|
||||||
|
const selfIsBlockquote = 'type' in chunk && chunk.type === 'blockquote';
|
||||||
|
|
||||||
let children = text;
|
let children = text;
|
||||||
|
|
||||||
@ -67,12 +85,11 @@ export default function serializerMarkdown(chunk: MdBlockType | MdLeafType, opts
|
|||||||
}
|
}
|
||||||
|
|
||||||
children = chunk.children
|
children = chunk.children
|
||||||
.map((c: MdBlockType | MdLeafType) => {
|
.map((c: MdBlockType | MdLeafType, childIndex) => {
|
||||||
const selfIsTable = type === NodeTypes.table;
|
const selfIsTable = type === NodeTypes.table;
|
||||||
const isList = !isLeafNode(c) ? (LIST_TYPES as string[]).includes(c.type || '') : false;
|
const isList = !isLeafNode(c) ? (LIST_TYPES as string[]).includes(c.type || '') : false;
|
||||||
const selfIsList = (LIST_TYPES as string[]).includes(chunk.type || '');
|
const selfIsList = (LIST_TYPES as string[]).includes(chunk.type || '');
|
||||||
const selfIsCode = (CODE_ELEMENTS as string[]).includes(chunk.type || '');
|
const selfIsCode = (CODE_ELEMENTS as string[]).includes(chunk.type || '');
|
||||||
const selfIsBlockquote = chunk.type === 'blockquote';
|
|
||||||
|
|
||||||
// Links can have the following shape
|
// Links can have the following shape
|
||||||
// In which case we don't want to surround
|
// In which case we don't want to surround
|
||||||
@ -91,7 +108,7 @@ export default function serializerMarkdown(chunk: MdBlockType | MdLeafType, opts
|
|||||||
childrenHasLink = chunk.children.some(f => !isLeafNode(f) && f.type === NodeTypes.link);
|
childrenHasLink = chunk.children.some(f => !isLeafNode(f) && f.type === NodeTypes.link);
|
||||||
}
|
}
|
||||||
|
|
||||||
return serializerMarkdown(
|
return serializeMarkdownNode(
|
||||||
{ ...c, parentType: type },
|
{ ...c, parentType: type },
|
||||||
{
|
{
|
||||||
// WOAH.
|
// WOAH.
|
||||||
@ -102,20 +119,18 @@ export default function serializerMarkdown(chunk: MdBlockType | MdLeafType, opts
|
|||||||
// of whitespace. If we're parallel to a link we also don't want
|
// of whitespace. If we're parallel to a link we also don't want
|
||||||
// to respect neighboring paragraphs
|
// to respect neighboring paragraphs
|
||||||
ignoreParagraphNewline:
|
ignoreParagraphNewline:
|
||||||
(ignoreParagraphNewline || isList || selfIsList || childrenHasLink) &&
|
(ignoreParagraphNewline || isList || selfIsList || childrenHasLink || isInTable) &&
|
||||||
// if we have c.break, never ignore empty paragraph new line
|
// if we have c.break, never ignore empty paragraph new line
|
||||||
!(c as MdBlockType).break,
|
!(c as MdBlockType).break,
|
||||||
|
|
||||||
// track depth of nested lists so we can add proper spacing
|
// track depth of nested lists so we can add proper spacing
|
||||||
listDepth: (LIST_TYPES as string[]).includes((c as MdBlockType).type || '')
|
listDepth: selfIsList ? listDepth + 1 : listDepth,
|
||||||
? listDepth + 1
|
|
||||||
: listDepth,
|
|
||||||
|
|
||||||
isInTable: selfIsTable || isInTable,
|
isInTable: selfIsTable || isInTable,
|
||||||
|
|
||||||
isInCode: selfIsCode || isInCode,
|
isInCode: selfIsCode || isInCode,
|
||||||
|
|
||||||
blockquoteDepth: selfIsBlockquote ? blockquoteDepth + 1 : blockquoteDepth,
|
blockquoteDepth: selfIsBlockquote ? blockquoteDepth + 1 : blockquoteDepth,
|
||||||
|
useMdx,
|
||||||
|
index: childIndex,
|
||||||
|
shortcodeConfigs,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -127,7 +142,10 @@ export default function serializerMarkdown(chunk: MdBlockType | MdLeafType, opts
|
|||||||
!ignoreParagraphNewline &&
|
!ignoreParagraphNewline &&
|
||||||
(text === '' || text === '\n') &&
|
(text === '' || text === '\n') &&
|
||||||
chunk.parentType === NodeTypes.paragraph &&
|
chunk.parentType === NodeTypes.paragraph &&
|
||||||
type !== NodeTypes.image
|
type !== NodeTypes.image &&
|
||||||
|
type !== NodeTypes.shortcode &&
|
||||||
|
type !== NodeTypes.tableCell &&
|
||||||
|
type !== NodeTypes.tableHeaderCell
|
||||||
) {
|
) {
|
||||||
type = NodeTypes.paragraph;
|
type = NodeTypes.paragraph;
|
||||||
children = '\n';
|
children = '\n';
|
||||||
@ -145,7 +163,6 @@ export default function serializerMarkdown(chunk: MdBlockType | MdLeafType, opts
|
|||||||
// "Text foo bar **baz**" resulting in "**Text foo bar **baz****"
|
// "Text foo bar **baz**" resulting in "**Text foo bar **baz****"
|
||||||
// which is invalid markup and can mess everything up
|
// which is invalid markup and can mess everything up
|
||||||
if (children !== '\n' && isLeafNode(chunk)) {
|
if (children !== '\n' && isLeafNode(chunk)) {
|
||||||
children = isInCode || chunk.code ? children : escapeHtml(children);
|
|
||||||
if (chunk.strikethrough && chunk.bold && chunk.italic) {
|
if (chunk.strikethrough && chunk.bold && chunk.italic) {
|
||||||
children = retainWhitespaceAndFormat(children, '~~***');
|
children = retainWhitespaceAndFormat(children, '~~***');
|
||||||
} else if (chunk.bold && chunk.italic) {
|
} else if (chunk.bold && chunk.italic) {
|
||||||
@ -220,46 +237,44 @@ export default function serializerMarkdown(chunk: MdBlockType | MdLeafType, opts
|
|||||||
return `###### ${children}\n`;
|
return `###### ${children}\n`;
|
||||||
|
|
||||||
case NodeTypes.block_quote:
|
case NodeTypes.block_quote:
|
||||||
// For some reason, marked is parsing blockquotes w/ one new line
|
return `${selfIsBlockquote && blockquoteDepth > 0 ? '\n' : ''}> ${children
|
||||||
// as contiued blockquotes, so adding two new lines ensures that doesn't
|
|
||||||
// happen
|
|
||||||
return `> ${children
|
|
||||||
.replace(/^[\n]*|[\n]*$/gm, '')
|
.replace(/^[\n]*|[\n]*$/gm, '')
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.join('\n> ')}\n\n`;
|
.join('\n> ')}\n`;
|
||||||
|
|
||||||
case NodeTypes.code_block:
|
case NodeTypes.code_block:
|
||||||
const codeBlock = chunk as MdCodeBlockElement;
|
const codeBlock = chunk as MdCodeBlockElement;
|
||||||
return `\`\`\`${codeBlock.lang ?? ''}\n${codeBlock.code}\n\`\`\`\n`;
|
return `\`\`\`${codeBlock.lang ?? ''}\n${codeBlock.code}\n\`\`\`\n`;
|
||||||
|
|
||||||
case NodeTypes.link:
|
case NodeTypes.link:
|
||||||
const linkBlock = chunk as unknown as MdLinkElement;
|
const linkBlock = chunk as MdLinkElement;
|
||||||
return `[${children}](${linkBlock.url || ''})`;
|
return `[${children}](${linkBlock.url || ''})`;
|
||||||
|
|
||||||
case NodeTypes.image:
|
case NodeTypes.image:
|
||||||
const imageBlock = chunk as unknown as MdImageElement;
|
const imageBlock = chunk as MdImageElement;
|
||||||
const caption = imageBlock.caption ?? [];
|
const alt = imageBlock.alt ?? '';
|
||||||
return `![${caption.length > 0 ? caption[0].text ?? '' : ''}](${imageBlock.url || ''})`;
|
return `![${alt}](${imageBlock.url || ''})`;
|
||||||
|
|
||||||
case NodeTypes.ul_list:
|
case NodeTypes.ul_list:
|
||||||
case NodeTypes.ol_list:
|
case NodeTypes.ol_list:
|
||||||
return `\n${children}`;
|
return `${listDepth > 0 ? '\n' : ''}${children}`;
|
||||||
|
|
||||||
case NodeTypes.listItemContent:
|
case NodeTypes.listItemContent:
|
||||||
return children;
|
return children;
|
||||||
|
|
||||||
case NodeTypes.listItem:
|
case NodeTypes.listItem:
|
||||||
const listItemBlock = chunk as unknown as MdListItemElement;
|
const listItemBlock = chunk as MdListItemElement;
|
||||||
|
|
||||||
const isOL = chunk && chunk.parentType === NodeTypes.ol_list;
|
const isOL = chunk && chunk.parentType === NodeTypes.ol_list;
|
||||||
|
|
||||||
const treatAsLeaf =
|
const treatAsLeaf =
|
||||||
(chunk as MdBlockType).children.length >= 1 &&
|
(chunk as MdBlockType).children.length >= 1 &&
|
||||||
((chunk as MdBlockType).children.reduce((acc, child) => acc && isLeafNode(child), true) ||
|
((chunk as MdBlockType).children.reduce((acc, child) => acc && isLeafNode(child), true) ||
|
||||||
((chunk as MdBlockType).children[0] as BlockType).type === 'lic');
|
((chunk as MdBlockType).children.length === 1 &&
|
||||||
|
((chunk as MdBlockType).children[0] as BlockType).type === 'lic'));
|
||||||
|
|
||||||
let spacer = '';
|
let spacer = '';
|
||||||
for (let k = 0; listDepth > k; k++) {
|
for (let k = 1; listDepth > k; k++) {
|
||||||
if (isOL) {
|
if (isOL) {
|
||||||
// https://github.com/remarkjs/remark-react/issues/65
|
// https://github.com/remarkjs/remark-react/issues/65
|
||||||
spacer += ' ';
|
spacer += ' ';
|
||||||
@ -270,15 +285,19 @@ export default function serializerMarkdown(chunk: MdBlockType | MdLeafType, opts
|
|||||||
|
|
||||||
let checkbox = '';
|
let checkbox = '';
|
||||||
if (typeof listItemBlock.checked === 'boolean') {
|
if (typeof listItemBlock.checked === 'boolean') {
|
||||||
checkbox = ` [${listItemBlock.checked ? 'X' : ' '}]`;
|
checkbox = ` [${listItemBlock.checked ? 'x' : ' '}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${spacer}${isOL ? '1.' : '-'}${checkbox} ${children}${treatAsLeaf ? '\n' : ''}`;
|
return `${spacer}${isOL ? '1.' : '-'}${checkbox} ${children}${treatAsLeaf ? '\n' : ''}`;
|
||||||
|
|
||||||
case NodeTypes.paragraph:
|
case NodeTypes.paragraph:
|
||||||
const paragraphNode = chunk as unknown as MdParagraphElement;
|
const paragraphNode = chunk as MdParagraphElement;
|
||||||
if (paragraphNode.align) {
|
if (useMdx && paragraphNode.align) {
|
||||||
return `<p style={{ textAlign: '${paragraphNode.align}' }}>${children}</p>`;
|
return retainWhitespaceAndFormat(
|
||||||
|
children,
|
||||||
|
`<p style={{ textAlign: '${paragraphNode.align}' }}>`,
|
||||||
|
'</p>\n',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return `${children}${!isInTable ? '\n' : ''}`;
|
return `${children}${!isInTable ? '\n' : ''}`;
|
||||||
|
|
||||||
@ -287,15 +306,31 @@ export default function serializerMarkdown(chunk: MdBlockType | MdLeafType, opts
|
|||||||
|
|
||||||
case NodeTypes.table:
|
case NodeTypes.table:
|
||||||
const columns = getTableColumnCount(chunk as TableNode);
|
const columns = getTableColumnCount(chunk as TableNode);
|
||||||
return `|${Array(columns).fill(' ').join('|')}|
|
const rows = children.split('\n');
|
||||||
|
const header = rows.length > 0 ? rows[0] : `|${Array(columns).fill(' ').join('|')}|`;
|
||||||
|
const bodyRows = rows.slice(1);
|
||||||
|
|
||||||
|
return `${header}
|
||||||
|${Array(columns).fill('---').join('|')}|
|
|${Array(columns).fill('---').join('|')}|
|
||||||
${children}\n`;
|
${bodyRows.join('\n')}`;
|
||||||
|
|
||||||
case NodeTypes.tableRow:
|
case NodeTypes.tableRow:
|
||||||
return `|${children}|\n`;
|
return `|${children}|\n`;
|
||||||
|
|
||||||
|
case NodeTypes.tableHeaderCell:
|
||||||
case NodeTypes.tableCell:
|
case NodeTypes.tableCell:
|
||||||
return children.replace(/\|/g, '\\|').replace(/\n/g, BREAK_TAG);
|
return isEmpty(children) ? ' ' : children.replace(/\|/g, '\\|').replace(/\n/g, BREAK_TAG);
|
||||||
|
|
||||||
|
case NodeTypes.shortcode:
|
||||||
|
const shortcodeNode = chunk as MdShortcodeElement;
|
||||||
|
const shortcodeConfig = shortcodeConfigs[shortcodeNode.shortcode];
|
||||||
|
if (!shortcodeConfig) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${shortcodeConfig.openTag}${[shortcodeNode.shortcode, ...shortcodeNode.args].join(
|
||||||
|
shortcodeConfig.separator,
|
||||||
|
)}${shortcodeConfig.closeTag}`;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn('Unrecognized slate node, proceeding as text', `"${type}"`, chunk);
|
console.warn('Unrecognized slate node, proceeding as text', `"${type}"`, chunk);
|
||||||
@ -343,3 +378,23 @@ function getTableColumnCount(tableNode: TableNode): number {
|
|||||||
|
|
||||||
return rows[0].children.length;
|
return rows[0].children.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SerializeMarkdownOptions {
|
||||||
|
useMdx: boolean;
|
||||||
|
shortcodeConfigs?: Record<string, ShortcodeConfig<{}>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function serializeMarkdown(
|
||||||
|
slateValue: MdValue,
|
||||||
|
{ useMdx, shortcodeConfigs }: SerializeMarkdownOptions,
|
||||||
|
) {
|
||||||
|
return slateValue
|
||||||
|
.map((v, index) =>
|
||||||
|
serializeMarkdownNode(v as BlockType | LeafType, {
|
||||||
|
useMdx,
|
||||||
|
index,
|
||||||
|
shortcodeConfigs: shortcodeConfigs ?? getShortcodes(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
import { processShortcodeConfigToMdx } from '../processShortcodeConfig';
|
||||||
|
import { testShortcodeConfigs } from '../../../tests-util/serializationTests.util';
|
||||||
|
|
||||||
|
describe('processShortcodeConfig', () => {
|
||||||
|
describe('processShortcodeConfigToMdx', () => {
|
||||||
|
it('converts to mdx', () => {
|
||||||
|
const markdown = '[youtube|p6h-rYSVX90]';
|
||||||
|
const mdx = '<Shortcode shortcode="youtube" args={[\'p6h-rYSVX90\']} />';
|
||||||
|
|
||||||
|
expect(processShortcodeConfigToMdx(testShortcodeConfigs, markdown)).toBe(mdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts shortcode with no args', () => {
|
||||||
|
const markdown = '[youtube]';
|
||||||
|
const mdx = '<Shortcode shortcode="youtube" args={[]} />';
|
||||||
|
|
||||||
|
expect(processShortcodeConfigToMdx(testShortcodeConfigs, markdown)).toBe(mdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts shortcode with multiple args', () => {
|
||||||
|
const markdown = '[youtube|p6h-rYSVX90|somethingElse|andOneMore]';
|
||||||
|
const mdx =
|
||||||
|
"<Shortcode shortcode=\"youtube\" args={['p6h-rYSVX90', 'somethingElse', 'andOneMore']} />";
|
||||||
|
|
||||||
|
expect(processShortcodeConfigToMdx(testShortcodeConfigs, markdown)).toBe(mdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shortcode with text before', () => {
|
||||||
|
const markdown = 'Text before [youtube|p6h-rYSVX90]';
|
||||||
|
const mdx = 'Text before <Shortcode shortcode="youtube" args={[\'p6h-rYSVX90\']} />';
|
||||||
|
|
||||||
|
expect(processShortcodeConfigToMdx(testShortcodeConfigs, markdown)).toBe(mdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shortcode with text after', () => {
|
||||||
|
const markdown = '[youtube|p6h-rYSVX90] and text after';
|
||||||
|
const mdx = '<Shortcode shortcode="youtube" args={[\'p6h-rYSVX90\']} /> and text after';
|
||||||
|
|
||||||
|
expect(processShortcodeConfigToMdx(testShortcodeConfigs, markdown)).toBe(mdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shortcode with text before and after', () => {
|
||||||
|
const markdown = 'Text before [youtube|p6h-rYSVX90] and text after';
|
||||||
|
const mdx =
|
||||||
|
'Text before <Shortcode shortcode="youtube" args={[\'p6h-rYSVX90\']} /> and text after';
|
||||||
|
|
||||||
|
expect(processShortcodeConfigToMdx(testShortcodeConfigs, markdown)).toBe(mdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiple shortcodes', () => {
|
||||||
|
const markdown = 'Text before [youtube|p6h-rYSVX90] and {{< twitter 917359331535966209 >}}';
|
||||||
|
const mdx =
|
||||||
|
'Text before <Shortcode shortcode="youtube" args={[\'p6h-rYSVX90\']} /> and <Shortcode shortcode="twitter" args={[\'917359331535966209\']} />';
|
||||||
|
|
||||||
|
expect(processShortcodeConfigToMdx(testShortcodeConfigs, markdown)).toBe(mdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiple of the same shortcodes', () => {
|
||||||
|
const markdown =
|
||||||
|
'Text before [youtube|p6h-rYSVX90], [youtube|p6h-rYSVX90], {{< twitter 917359331535966209 >}} and [youtube|p6h-rYSVX90]';
|
||||||
|
const mdx =
|
||||||
|
'Text before <Shortcode shortcode="youtube" args={[\'p6h-rYSVX90\']} />, <Shortcode shortcode="youtube" args={[\'p6h-rYSVX90\']} />, <Shortcode shortcode="twitter" args={[\'917359331535966209\']} /> and <Shortcode shortcode="youtube" args={[\'p6h-rYSVX90\']} />';
|
||||||
|
|
||||||
|
expect(processShortcodeConfigToMdx(testShortcodeConfigs, markdown)).toBe(mdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unrecognized shortcode', () => {
|
||||||
|
const markdown = '[someOtherShortcode|andstuff]';
|
||||||
|
const mdx = '[someOtherShortcode|andstuff]';
|
||||||
|
|
||||||
|
expect(processShortcodeConfigToMdx(testShortcodeConfigs, markdown)).toBe(mdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unrecognized shortcode surrounded by recognized shortcodes', () => {
|
||||||
|
const markdown =
|
||||||
|
'Text before [youtube|p6h-rYSVX90], [someOtherShortcode|andstuff] and {{< twitter 917359331535966209 >}}';
|
||||||
|
const mdx =
|
||||||
|
'Text before <Shortcode shortcode="youtube" args={[\'p6h-rYSVX90\']} />, [someOtherShortcode|andstuff] and <Shortcode shortcode="twitter" args={[\'917359331535966209\']} />';
|
||||||
|
|
||||||
|
expect(processShortcodeConfigToMdx(testShortcodeConfigs, markdown)).toBe(mdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('plain text', () => {
|
||||||
|
const markdown = 'Some text about something going on somewhere';
|
||||||
|
const mdx = 'Some text about something going on somewhere';
|
||||||
|
|
||||||
|
expect(processShortcodeConfigToMdx(testShortcodeConfigs, markdown)).toBe(mdx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -15,10 +15,13 @@ import {
|
|||||||
ELEMENT_PARAGRAPH,
|
ELEMENT_PARAGRAPH,
|
||||||
ELEMENT_TABLE,
|
ELEMENT_TABLE,
|
||||||
ELEMENT_TD,
|
ELEMENT_TD,
|
||||||
|
ELEMENT_TH,
|
||||||
ELEMENT_TR,
|
ELEMENT_TR,
|
||||||
ELEMENT_UL,
|
ELEMENT_UL,
|
||||||
} from '@udecode/plate';
|
} from '@udecode/plate';
|
||||||
|
|
||||||
|
import { ELEMENT_SHORTCODE } from '../../plateTypes';
|
||||||
|
|
||||||
export const VOID_ELEMENTS = [ELEMENT_CODE_BLOCK, ELEMENT_IMAGE];
|
export const VOID_ELEMENTS = [ELEMENT_CODE_BLOCK, ELEMENT_IMAGE];
|
||||||
|
|
||||||
export const MarkNodeTypes = {
|
export const MarkNodeTypes = {
|
||||||
@ -39,6 +42,7 @@ export const NodeTypes = {
|
|||||||
table: ELEMENT_TABLE,
|
table: ELEMENT_TABLE,
|
||||||
tableRow: ELEMENT_TR,
|
tableRow: ELEMENT_TR,
|
||||||
tableCell: ELEMENT_TD,
|
tableCell: ELEMENT_TD,
|
||||||
|
tableHeaderCell: ELEMENT_TH,
|
||||||
heading: {
|
heading: {
|
||||||
1: ELEMENT_H1,
|
1: ELEMENT_H1,
|
||||||
2: ELEMENT_H2,
|
2: ELEMENT_H2,
|
||||||
@ -47,6 +51,7 @@ export const NodeTypes = {
|
|||||||
5: ELEMENT_H5,
|
5: ELEMENT_H5,
|
||||||
6: ELEMENT_H6,
|
6: ELEMENT_H6,
|
||||||
},
|
},
|
||||||
|
shortcode: ELEMENT_SHORTCODE,
|
||||||
emphasis_mark: 'italic',
|
emphasis_mark: 'italic',
|
||||||
strong_mark: 'bold',
|
strong_mark: 'bold',
|
||||||
delete_mark: 'strikethrough',
|
delete_mark: 'strikethrough',
|
||||||
@ -87,13 +92,19 @@ export interface BlockType {
|
|||||||
type: string;
|
type: string;
|
||||||
parentType?: string;
|
parentType?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
caption?: string;
|
alt?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
break?: boolean;
|
break?: boolean;
|
||||||
children: Array<BlockType | LeafType>;
|
children: Array<BlockType | LeafType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MdastNode = BaseMdastNode | MdxMdastNode;
|
export interface ShortcodeNode extends BaseMdastNode {
|
||||||
|
type: 'shortcode';
|
||||||
|
shortcode: string;
|
||||||
|
args: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MdastNode = BaseMdastNode | MdxMdastNode | ShortcodeNode;
|
||||||
|
|
||||||
export interface BaseMdastNode {
|
export interface BaseMdastNode {
|
||||||
type?: Omit<MdastNodeType, 'mdxJsxTextElement'>;
|
type?: Omit<MdastNodeType, 'mdxJsxTextElement'>;
|
||||||
@ -114,6 +125,7 @@ export interface BaseMdastNode {
|
|||||||
checked?: any;
|
checked?: any;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
indent?: any;
|
indent?: any;
|
||||||
|
align?: (string | null)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MdxMdastNodeAttributeValue {
|
export interface MdxMdastNodeAttributeValue {
|
||||||
@ -150,8 +162,6 @@ export interface MdxMdastNode extends BaseMdastNode {
|
|||||||
attributes?: MdxMdastNodeAttribute[];
|
attributes?: MdxMdastNodeAttribute[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const allowedStyles: string[] = ['color', 'backgroundColor'];
|
|
||||||
|
|
||||||
export interface TextNodeStyles {
|
export interface TextNodeStyles {
|
||||||
color?: string;
|
color?: string;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
@ -208,7 +218,7 @@ export type ImageNode = {
|
|||||||
type: typeof NodeTypes['image'];
|
type: typeof NodeTypes['image'];
|
||||||
children: Array<DeserializedNode>;
|
children: Array<DeserializedNode>;
|
||||||
url: string | undefined;
|
url: string | undefined;
|
||||||
caption: TextNode;
|
alt: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TableNode = {
|
export type TableNode = {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
/* eslint-disable no-case-declarations */
|
/* eslint-disable no-case-declarations */
|
||||||
import { ELEMENT_PARAGRAPH } from '@udecode/plate';
|
import { ELEMENT_PARAGRAPH } from '@udecode/plate';
|
||||||
|
|
||||||
import { allowedStyles, LIST_TYPES, MarkNodeTypes, NodeTypes } from './ast-types';
|
import { LIST_TYPES, MarkNodeTypes, NodeTypes } from './ast-types';
|
||||||
|
import { processShortcodeConfigToSlate } from './processShortcodeConfig';
|
||||||
|
|
||||||
|
import type { ShortcodeConfig } from '@staticcms/core/interface';
|
||||||
import type { MdBlockElement } from '@staticcms/markdown';
|
import type { MdBlockElement } from '@staticcms/markdown';
|
||||||
import type {
|
import type {
|
||||||
AlignMdxMdastNodeAttribute,
|
AlignMdxMdastNodeAttribute,
|
||||||
@ -18,6 +20,7 @@ import type {
|
|||||||
ListNode,
|
ListNode,
|
||||||
MarkNode,
|
MarkNode,
|
||||||
MdastNode,
|
MdastNode,
|
||||||
|
MdxMdastNode,
|
||||||
ParagraphNode,
|
ParagraphNode,
|
||||||
StyleMdxMdastNodeAttribute,
|
StyleMdxMdastNodeAttribute,
|
||||||
TextNode,
|
TextNode,
|
||||||
@ -54,21 +57,62 @@ function mdxToMark(mark: keyof typeof MarkNodeTypes, children: DeserializedNode[
|
|||||||
} as MarkNode;
|
} as MarkNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Options {
|
function parseStyleAttribute(node: MdxMdastNode, allowedStyles: Record<string, string>) {
|
||||||
isInTable?: boolean;
|
const styleAttribute = node.attributes?.find(
|
||||||
|
a => a.name === 'style',
|
||||||
|
) as StyleMdxMdastNodeAttribute;
|
||||||
|
const nodeStyles: TextNodeStyles = {};
|
||||||
|
if (styleAttribute) {
|
||||||
|
let styles: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
styles =
|
||||||
|
JSON.parse(
|
||||||
|
styleAttribute.value.value
|
||||||
|
.replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2": ')
|
||||||
|
.replace(/:[ ]*[']([^']+)[']/g, ': "$1"'),
|
||||||
|
) ?? {};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error parsing font styles (${styleAttribute.value.value})`, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(styles).map(key => {
|
||||||
|
if (key in allowedStyles) {
|
||||||
|
nodeStyles[allowedStyles[key] as keyof TextNodeStyles] = styles[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeStyles;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function deserializeMarkdown(node: MdastNode, options?: Options) {
|
export interface Options {
|
||||||
|
isInTable?: boolean;
|
||||||
|
isInTableHeaderRow?: boolean;
|
||||||
|
tableAlign?: (string | null)[];
|
||||||
|
useMdx: boolean;
|
||||||
|
shortcodeConfigs: Record<string, ShortcodeConfig>;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function deserializeMarkdown(node: MdastNode, options: Options) {
|
||||||
let children: Array<DeserializedNode> = [{ text: '' }];
|
let children: Array<DeserializedNode> = [{ text: '' }];
|
||||||
|
|
||||||
const { isInTable = false } = options ?? {};
|
const {
|
||||||
|
isInTable = false,
|
||||||
|
isInTableHeaderRow = false,
|
||||||
|
tableAlign,
|
||||||
|
useMdx,
|
||||||
|
shortcodeConfigs,
|
||||||
|
index,
|
||||||
|
} = options ?? {};
|
||||||
|
|
||||||
const selfIsTable = node.type === 'table';
|
const selfIsTable = node.type === 'table';
|
||||||
|
const selfIsTableHeaderRow = node.type === 'tableRow' && index === 0;
|
||||||
|
|
||||||
const nodeChildren = node.children;
|
const nodeChildren = node.children;
|
||||||
if (nodeChildren && Array.isArray(nodeChildren) && nodeChildren.length > 0) {
|
if (nodeChildren && Array.isArray(nodeChildren) && nodeChildren.length > 0) {
|
||||||
children = nodeChildren.flatMap(
|
children = nodeChildren.flatMap(
|
||||||
(c: MdastNode) =>
|
(c: MdastNode, childIndex) =>
|
||||||
deserializeMarkdown(
|
deserializeMarkdown(
|
||||||
{
|
{
|
||||||
...c,
|
...c,
|
||||||
@ -76,6 +120,11 @@ export default function deserializeMarkdown(node: MdastNode, options?: Options)
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
isInTable: selfIsTable || isInTable,
|
isInTable: selfIsTable || isInTable,
|
||||||
|
isInTableHeaderRow: selfIsTableHeaderRow || isInTableHeaderRow,
|
||||||
|
useMdx,
|
||||||
|
shortcodeConfigs,
|
||||||
|
index: childIndex,
|
||||||
|
tableAlign: tableAlign || (selfIsTable ? node.align : undefined),
|
||||||
},
|
},
|
||||||
) as DeserializedNode,
|
) as DeserializedNode,
|
||||||
);
|
);
|
||||||
@ -152,7 +201,7 @@ export default function deserializeMarkdown(node: MdastNode, options?: Options)
|
|||||||
type: NodeTypes.image,
|
type: NodeTypes.image,
|
||||||
children: [{ text: '' }],
|
children: [{ text: '' }],
|
||||||
url: node.url,
|
url: node.url,
|
||||||
caption: [{ text: node.alt ?? '' }],
|
alt: node.alt,
|
||||||
} as ImageNode;
|
} as ImageNode;
|
||||||
|
|
||||||
case 'blockquote':
|
case 'blockquote':
|
||||||
@ -213,7 +262,23 @@ export default function deserializeMarkdown(node: MdastNode, options?: Options)
|
|||||||
return { type: NodeTypes.tableRow, children };
|
return { type: NodeTypes.tableRow, children };
|
||||||
|
|
||||||
case 'tableCell':
|
case 'tableCell':
|
||||||
return { type: NodeTypes.tableCell, children: [{ type: NodeTypes.paragraph, children }] };
|
return {
|
||||||
|
type: isInTableHeaderRow ? NodeTypes.tableHeaderCell : NodeTypes.tableCell,
|
||||||
|
children: [{ type: NodeTypes.paragraph, children }],
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'mdxJsxFlowElement':
|
||||||
|
if ('name' in node) {
|
||||||
|
switch (node.name) {
|
||||||
|
case 'br':
|
||||||
|
return { type: NodeTypes.paragraph, children: [{ text: '' }] };
|
||||||
|
default:
|
||||||
|
console.warn('unrecognized mdx flow element', node);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: node.value || '' };
|
||||||
|
|
||||||
case 'mdxJsxTextElement':
|
case 'mdxJsxTextElement':
|
||||||
if ('name' in node) {
|
if ('name' in node) {
|
||||||
@ -227,6 +292,8 @@ export default function deserializeMarkdown(node: MdastNode, options?: Options)
|
|||||||
case 'u':
|
case 'u':
|
||||||
return mdxToMark('underline_mark', children);
|
return mdxToMark('underline_mark', children);
|
||||||
case 'p':
|
case 'p':
|
||||||
|
const paragraphNodeStyles = parseStyleAttribute(node, { textAlign: 'align' });
|
||||||
|
|
||||||
const alignAttribute = node.attributes?.find(
|
const alignAttribute = node.attributes?.find(
|
||||||
a => a.name === 'align',
|
a => a.name === 'align',
|
||||||
) as AlignMdxMdastNodeAttribute;
|
) as AlignMdxMdastNodeAttribute;
|
||||||
@ -237,6 +304,7 @@ export default function deserializeMarkdown(node: MdastNode, options?: Options)
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
type: NodeTypes.paragraph,
|
type: NodeTypes.paragraph,
|
||||||
|
...paragraphNodeStyles,
|
||||||
...pNodeStyles,
|
...pNodeStyles,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@ -246,44 +314,25 @@ export default function deserializeMarkdown(node: MdastNode, options?: Options)
|
|||||||
],
|
],
|
||||||
} as ParagraphNode;
|
} as ParagraphNode;
|
||||||
case 'font':
|
case 'font':
|
||||||
const styleAttribute = node.attributes?.find(
|
const fontNodeStyles = parseStyleAttribute(node, {
|
||||||
a => a.name === 'style',
|
color: 'color',
|
||||||
) as StyleMdxMdastNodeAttribute;
|
backgroundColor: 'backgroundColor',
|
||||||
const nodeStyles: TextNodeStyles = {};
|
|
||||||
if (styleAttribute) {
|
|
||||||
let styles: Record<string, string> = {};
|
|
||||||
try {
|
|
||||||
styles =
|
|
||||||
JSON.parse(
|
|
||||||
styleAttribute.value.value
|
|
||||||
.replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2": ')
|
|
||||||
.replace(/:[ ]*[']([^']+)[']/g, ': "$1"'),
|
|
||||||
) ?? {};
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Error parsing font styles (${styleAttribute.value.value})`, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(styles).map(key => {
|
|
||||||
if (allowedStyles.includes(key)) {
|
|
||||||
nodeStyles[key as keyof TextNodeStyles] = styles[key];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const colorAttribute = node.attributes?.find(
|
const colorAttribute = node.attributes?.find(
|
||||||
a => a.name === 'color',
|
a => a.name === 'color',
|
||||||
) as ColorMdxMdastNodeAttribute;
|
) as ColorMdxMdastNodeAttribute;
|
||||||
if (colorAttribute) {
|
if (colorAttribute) {
|
||||||
nodeStyles.color = colorAttribute.value;
|
fontNodeStyles.color = colorAttribute.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...nodeStyles,
|
...fontNodeStyles,
|
||||||
...forceLeafNode(children as Array<TextNode>),
|
...forceLeafNode(children as Array<TextNode>),
|
||||||
...persistLeafFormats(children as Array<MdastNode>),
|
...persistLeafFormats(children as Array<MdastNode>),
|
||||||
} as TextNode;
|
} as TextNode;
|
||||||
default:
|
default:
|
||||||
console.warn('unrecognized mdx node', node);
|
console.warn('unrecognized mdx text element', node);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -291,7 +340,22 @@ export default function deserializeMarkdown(node: MdastNode, options?: Options)
|
|||||||
return { text: node.value || '' };
|
return { text: node.value || '' };
|
||||||
|
|
||||||
case 'text':
|
case 'text':
|
||||||
|
if (useMdx) {
|
||||||
return { text: node.value || '' };
|
return { text: node.value || '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.value) {
|
||||||
|
return { text: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodes: MdastNode[] = [node];
|
||||||
|
|
||||||
|
for (const shortcode in shortcodeConfigs) {
|
||||||
|
nodes = processShortcodeConfigToSlate(shortcode, shortcodeConfigs[shortcode], nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes.map(node => (node.type === 'text' ? { text: node.value ?? '' } : node));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn('Unrecognized mdast node, proceeding as text', node);
|
console.warn('Unrecognized mdast node, proceeding as text', node);
|
||||||
return { text: node.value || '' };
|
return { text: node.value || '' };
|
||||||
|
@ -2,4 +2,5 @@ export * from './ast-types';
|
|||||||
export * from './deserializeMarkdown';
|
export * from './deserializeMarkdown';
|
||||||
export { default as deserializeMarkdown } from './deserializeMarkdown';
|
export { default as deserializeMarkdown } from './deserializeMarkdown';
|
||||||
export { default as flattenListItemParagraphs } from './flattenListItemParagraphs';
|
export { default as flattenListItemParagraphs } from './flattenListItemParagraphs';
|
||||||
|
export * from './toSlatePlugin';
|
||||||
export { default as toSlatePlugin } from './toSlatePlugin';
|
export { default as toSlatePlugin } from './toSlatePlugin';
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
/* eslint-disable no-case-declarations */
|
||||||
|
import { isEmpty, isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
|
|
||||||
|
import type { ShortcodeConfig } from '@staticcms/core/interface';
|
||||||
|
import type { BaseMdastNode, MdastNode } from './ast-types';
|
||||||
|
|
||||||
|
function cleanRegex(str: string) {
|
||||||
|
return str
|
||||||
|
.replace('[', '\\[')
|
||||||
|
.replace(']', '\\]')
|
||||||
|
.replace('(', '\\(')
|
||||||
|
.replace(')', '\\)')
|
||||||
|
.replace('|', '\\|');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createShortcodeRegex(name: string, config: ShortcodeConfig) {
|
||||||
|
return `${cleanRegex(config.openTag)}(${name})${cleanRegex(
|
||||||
|
config.separator,
|
||||||
|
)}?([\\w\\W]*?)${cleanRegex(config.closeTag)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processShortcodeConfigToSlate(
|
||||||
|
name: string,
|
||||||
|
config: ShortcodeConfig,
|
||||||
|
nodes: BaseMdastNode[],
|
||||||
|
) {
|
||||||
|
const output: MdastNode[] = [];
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.value) {
|
||||||
|
const regex = new RegExp(`([\\w\\W]*?)${createShortcodeRegex(name, config)}([\\w\\W]*)`, 'g');
|
||||||
|
|
||||||
|
let matches: RegExpExecArray | null;
|
||||||
|
let rest = node.value;
|
||||||
|
while (isNotEmpty(rest) && (matches = regex.exec(rest)) !== null && matches.length === 5) {
|
||||||
|
const args = matches[3].trim();
|
||||||
|
if (isNotEmpty(matches[1])) {
|
||||||
|
output.push({
|
||||||
|
type: 'text',
|
||||||
|
value: matches[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push({
|
||||||
|
type: 'shortcode',
|
||||||
|
shortcode: name,
|
||||||
|
args: isEmpty(args) ? [] : args.split(config.separator),
|
||||||
|
children: [{ text: '' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
rest = matches[4];
|
||||||
|
regex.lastIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNotEmpty(rest)) {
|
||||||
|
output.push({
|
||||||
|
type: 'text',
|
||||||
|
value: rest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processShortcodeConfigToMdx(
|
||||||
|
configs: Record<string, ShortcodeConfig>,
|
||||||
|
markdown: string,
|
||||||
|
) {
|
||||||
|
if (isEmpty(markdown)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = markdown;
|
||||||
|
|
||||||
|
for (const name in configs) {
|
||||||
|
const config = configs[name];
|
||||||
|
const regex = new RegExp(createShortcodeRegex(name, config), 'g');
|
||||||
|
|
||||||
|
let matches: RegExpExecArray | null;
|
||||||
|
while ((matches = regex.exec(markdown)) !== null && matches.length === 3) {
|
||||||
|
const args = isEmpty(matches[2]) ? [] : matches[2]?.split(config.separator);
|
||||||
|
const argsOutput = args?.length > 0 ? `'${args.join("', '")}'` : '';
|
||||||
|
|
||||||
|
output = output.replace(
|
||||||
|
matches[0],
|
||||||
|
`<Shortcode shortcode="${matches[1]}" args={[${argsOutput}]} />`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
@ -1,14 +1,21 @@
|
|||||||
import transform from './deserializeMarkdown';
|
import transform from './deserializeMarkdown';
|
||||||
|
|
||||||
|
import type { ShortcodeConfig } from '@staticcms/core/interface';
|
||||||
import type { Plugin } from 'unified';
|
import type { Plugin } from 'unified';
|
||||||
import type { MdastNode } from './ast-types';
|
import type { MdastNode } from './ast-types';
|
||||||
|
|
||||||
const toSlatePlugin: Plugin = function () {
|
export interface ToSlatePluginOptions {
|
||||||
|
shortcodeConfigs: Record<string, ShortcodeConfig>;
|
||||||
|
useMdx: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toSlatePlugin = ({ shortcodeConfigs, useMdx }: ToSlatePluginOptions): Plugin =>
|
||||||
|
function () {
|
||||||
const compiler = (node: { children: Array<MdastNode> }) => {
|
const compiler = (node: { children: Array<MdastNode> }) => {
|
||||||
return node.children.map(c => transform(c, {}));
|
return node.children.map((c, index) => transform(c, { shortcodeConfigs, useMdx, index }));
|
||||||
};
|
};
|
||||||
|
|
||||||
this.Compiler = compiler;
|
this.Compiler = compiler;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default toSlatePlugin;
|
export default toSlatePlugin;
|
||||||
|
File diff suppressed because it is too large
Load Diff
127
core/src/widgets/markdown/withMarkdownControl.tsx
Normal file
127
core/src/widgets/markdown/withMarkdownControl.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import FieldLabel from '@staticcms/core/components/UI/FieldLabel';
|
||||||
|
import Outline from '@staticcms/core/components/UI/Outline';
|
||||||
|
import useDebounce from '../../lib/hooks/useDebounce';
|
||||||
|
import useMarkdownToSlate from './plate/hooks/useMarkdownToSlate';
|
||||||
|
import PlateEditor from './plate/PlateEditor';
|
||||||
|
import serializeMarkdown from './plate/serialization/serializeMarkdown';
|
||||||
|
|
||||||
|
import type { MarkdownField, WidgetControlProps } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import type { MdValue } from './plate/plateTypes';
|
||||||
|
|
||||||
|
const StyledEditorWrapper = styled('div')`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.toastui-editor-main .toastui-editor-md-vertical-style .toastui-editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastui-editor-main .toastui-editor-md-splitter {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastui-editor-md-preview {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastui-editor-defaultUI {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface WithMarkdownControlProps {
|
||||||
|
useMdx: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withMarkdownControl = ({ useMdx }: WithMarkdownControlProps) => {
|
||||||
|
const MarkdownControl: FC<WidgetControlProps<string, MarkdownField>> = controlProps => {
|
||||||
|
const { label, value, onChange, hasErrors, collection, entry, field } = controlProps;
|
||||||
|
|
||||||
|
const [internalValue, setInternalValue] = useState(value ?? '');
|
||||||
|
const [hasFocus, setHasFocus] = useState(false);
|
||||||
|
const debouncedFocus = useDebounce(hasFocus, 150);
|
||||||
|
|
||||||
|
const handleOnFocus = useCallback(() => {
|
||||||
|
setHasFocus(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOnBlur = useCallback(() => {
|
||||||
|
setHasFocus(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOnChange = useCallback(
|
||||||
|
(slateValue: MdValue) => {
|
||||||
|
const newValue = serializeMarkdown(slateValue, { useMdx });
|
||||||
|
if (newValue !== internalValue) {
|
||||||
|
setInternalValue(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[internalValue, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLabelClick = useCallback(() => {
|
||||||
|
// editorRef.current?.getInstance().focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [slateValue, loaded] = useMarkdownToSlate(internalValue, { useMdx });
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => (
|
||||||
|
<StyledEditorWrapper key="markdown-control-wrapper">
|
||||||
|
<FieldLabel
|
||||||
|
key="markdown-control-label"
|
||||||
|
isActive={hasFocus}
|
||||||
|
hasErrors={hasErrors}
|
||||||
|
onClick={handleLabelClick}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</FieldLabel>
|
||||||
|
{loaded ? (
|
||||||
|
<PlateEditor
|
||||||
|
initialValue={slateValue}
|
||||||
|
collection={collection}
|
||||||
|
entry={entry}
|
||||||
|
field={field}
|
||||||
|
useMdx={useMdx}
|
||||||
|
controlProps={controlProps}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
onFocus={handleOnFocus}
|
||||||
|
onBlur={handleOnBlur}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Outline
|
||||||
|
key="markdown-control-outline"
|
||||||
|
hasLabel
|
||||||
|
hasError={hasErrors}
|
||||||
|
active={hasFocus || debouncedFocus}
|
||||||
|
/>
|
||||||
|
</StyledEditorWrapper>
|
||||||
|
),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[
|
||||||
|
collection,
|
||||||
|
controlProps,
|
||||||
|
debouncedFocus,
|
||||||
|
field,
|
||||||
|
handleLabelClick,
|
||||||
|
handleOnBlur,
|
||||||
|
handleOnChange,
|
||||||
|
handleOnFocus,
|
||||||
|
hasErrors,
|
||||||
|
hasFocus,
|
||||||
|
label,
|
||||||
|
loaded,
|
||||||
|
slateValue,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return MarkdownControl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withMarkdownControl;
|
22
core/src/widgets/mdx/index.ts
Normal file
22
core/src/widgets/mdx/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import withMarkdownControl from '../markdown/withMarkdownControl';
|
||||||
|
import previewComponent from '../markdown/MarkdownPreview';
|
||||||
|
import schema from '../markdown/schema';
|
||||||
|
|
||||||
|
import type { MarkdownField, WidgetParam } from '@staticcms/core/interface';
|
||||||
|
|
||||||
|
const controlComponent = withMarkdownControl({ useMdx: true });
|
||||||
|
|
||||||
|
const MdxWidget = (): WidgetParam<string, MarkdownField> => {
|
||||||
|
return {
|
||||||
|
name: 'mdx',
|
||||||
|
controlComponent,
|
||||||
|
previewComponent,
|
||||||
|
options: {
|
||||||
|
schema,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { controlComponent as MdxControl, previewComponent as MdxPreview, schema as MdxSchema };
|
||||||
|
|
||||||
|
export default MdxWidget;
|
@ -10,5 +10,8 @@ if (typeof window === 'undefined') {
|
|||||||
removeItem: jest.fn(),
|
removeItem: jest.fn(),
|
||||||
getItem: jest.fn(),
|
getItem: jest.fn(),
|
||||||
},
|
},
|
||||||
|
navigator: {
|
||||||
|
platform: 'Win',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
"declarationDir": "dist",
|
"declarationDir": "dist",
|
||||||
"emitDeclarationOnly": true,
|
"emitDeclarationOnly": true,
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"target": "esnext",
|
"target": "ES2020",
|
||||||
"module": "esnext",
|
"module": "ES2020",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"preserveSymlinks": true,
|
"preserveSymlinks": true,
|
||||||
@ -17,7 +17,7 @@
|
|||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"lib": ["DOM", "es6", "ES2015"],
|
"lib": ["DOM", "es6", "ES2015", "ES2020"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@staticcms/boolean": ["./src/widgets/boolean"],
|
"@staticcms/boolean": ["./src/widgets/boolean"],
|
||||||
"@staticcms/boolean/*": ["./src/widgets/boolean/*"],
|
"@staticcms/boolean/*": ["./src/widgets/boolean/*"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user