fix: blockquote newline (#894)

This commit is contained in:
Daniel Lautzenheiser 2023-09-24 10:34:16 -04:00 committed by GitHub
parent d1cf12ec84
commit 69d11e831e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 317 additions and 62 deletions

View File

@ -1,7 +1,10 @@
import { isNotNullish, isNullish } from './null.util'; import { isNotNullish, isNullish } from './null.util';
export function isEmpty(value: string | null | undefined): value is null | undefined { export function isEmpty(
return isNullish(value) || value === ''; value: string | null | undefined,
ignoreWhitespace?: boolean,
): value is null | undefined {
return isNullish(value) || (ignoreWhitespace ? value.trim() === '' : value === '');
} }
export function isNotEmpty(value: string | null | undefined): value is string { export function isNotEmpty(value: string | null | undefined): value is string {

View File

@ -2,5 +2,6 @@
@apply border-l-2 @apply border-l-2
border-gray-400 border-gray-400
ml-2 ml-2
pl-2; pl-2
my-2;
} }

View File

@ -6,7 +6,6 @@ import type { MdPlatePlugin } from '@staticcms/markdown';
const softBreakPlugin: Partial<MdPlatePlugin<SoftBreakPlugin>> = { const softBreakPlugin: Partial<MdPlatePlugin<SoftBreakPlugin>> = {
options: { options: {
rules: [ rules: [
{ hotkey: 'shift+enter' },
{ {
hotkey: 'enter', hotkey: 'enter',
query: { query: {

View File

@ -108,7 +108,9 @@ function serializeMarkdownNode(
childrenHasLink = chunk.children.some(f => !isLeafNode(f) && f.type === NodeTypes.link); childrenHasLink = chunk.children.some(f => !isLeafNode(f) && f.type === NodeTypes.link);
} }
return serializeMarkdownNode( return {
type: 'type' in c ? c.type : undefined,
response: serializeMarkdownNode(
{ ...c, parentType: type }, { ...c, parentType: type },
{ {
// WOAH. // WOAH.
@ -132,9 +134,34 @@ function serializeMarkdownNode(
index: childIndex, index: childIndex,
shortcodeConfigs, shortcodeConfigs,
}, },
); ),
};
}) })
.join(separator); .map(({ response, type }) => {
if (selfIsBlockquote) {
let serializedChild = response;
if (listDepth === 0) {
serializedChild = serializedChild.replace(
/(?<!(?:[ ]*(?:-|1.) [^\n]*)|[\n])[\n]{1}([^\n])/g,
' \n$1',
);
}
return { response: serializedChild, type };
}
return { response, type };
})
.reduce((acc, { response, type }, index) => {
if (selfIsBlockquote) {
if (type === NodeTypes.block_quote) {
return index === 0 ? response : `${acc}${separator}\n${response}`;
}
}
return index === 0 ? response : `${acc}${separator}${response}`;
}, '');
} }
// This is pretty fragile code, check the long comment where we iterate over children // This is pretty fragile code, check the long comment where we iterate over children
@ -152,7 +179,7 @@ function serializeMarkdownNode(
} }
if (children === '' && !VOID_ELEMENTS.find(k => NodeTypes[k] === type)) { if (children === '' && !VOID_ELEMENTS.find(k => NodeTypes[k] === type)) {
return; return '\n';
} }
// Never allow decorating break tags with rich text formatting, // Never allow decorating break tags with rich text formatting,
@ -237,10 +264,11 @@ function serializeMarkdownNode(
return `###### ${handleInBlockNewline(children)}\n`; return `###### ${handleInBlockNewline(children)}\n`;
case NodeTypes.block_quote: case NodeTypes.block_quote:
return `${selfIsBlockquote && blockquoteDepth > 0 ? '\n' : ''}> ${children return `> ${children
.replace(/^[\n]*|[\n]*$/gm, '') .replace(/[\n]+$/g, '')
.split('\n') .split('\n')
.join('\n> ')}\n`; .join('\n> ')
.replace(/\n>[ \t]*\n/g, '\n>\n')}${selfIsBlockquote && blockquoteDepth === 0 ? '\n' : ''}`;
case NodeTypes.code_block: case NodeTypes.code_block:
const codeBlock = chunk as MdCodeBlockElement; const codeBlock = chunk as MdCodeBlockElement;
@ -319,7 +347,9 @@ ${bodyRows.join('\n')}`;
case NodeTypes.tableHeaderCell: case NodeTypes.tableHeaderCell:
case NodeTypes.tableCell: case NodeTypes.tableCell:
return isEmpty(children) ? ' ' : children.replace(/\|/g, '\\|').replace(/\n/g, BREAK_TAG); return isEmpty(children, true)
? ' '
: children.replace(/\|/g, '\\|').replace(/\n/g, BREAK_TAG);
case NodeTypes.shortcode: case NodeTypes.shortcode:
const shortcodeNode = chunk as MdShortcodeElement; const shortcodeNode = chunk as MdShortcodeElement;

View File

@ -92,6 +92,8 @@ export interface Options {
isInTable?: boolean; isInTable?: boolean;
isInLink?: boolean; isInLink?: boolean;
isInTableHeaderRow?: boolean; isInTableHeaderRow?: boolean;
isInBlockquote?: boolean;
isInList?: boolean;
tableAlign?: (string | null)[]; tableAlign?: (string | null)[];
useMdx: boolean; useMdx: boolean;
shortcodeConfigs: Record<string, ShortcodeConfig>; shortcodeConfigs: Record<string, ShortcodeConfig>;
@ -105,6 +107,8 @@ export default function deserializeMarkdown(node: MdastNode, options: Options) {
isInTable = false, isInTable = false,
isInLink = false, isInLink = false,
isInTableHeaderRow = false, isInTableHeaderRow = false,
isInBlockquote = false,
isInList = false,
tableAlign, tableAlign,
useMdx, useMdx,
shortcodeConfigs, shortcodeConfigs,
@ -114,6 +118,8 @@ export default function deserializeMarkdown(node: MdastNode, options: Options) {
const selfIsTable = node.type === 'table'; const selfIsTable = node.type === 'table';
const selfIsLink = node.type === 'link'; const selfIsLink = node.type === 'link';
const selfIsTableHeaderRow = node.type === 'tableRow' && index === 0; const selfIsTableHeaderRow = node.type === 'tableRow' && index === 0;
const selfIsBlockquote = node.type === 'blockquote';
const selfIsList = node.type === 'list';
const nodeChildren = node.children; const nodeChildren = node.children;
if (nodeChildren && Array.isArray(nodeChildren) && nodeChildren.length > 0) { if (nodeChildren && Array.isArray(nodeChildren) && nodeChildren.length > 0) {
@ -128,6 +134,8 @@ export default function deserializeMarkdown(node: MdastNode, options: Options) {
isInTable: selfIsTable || isInTable, isInTable: selfIsTable || isInTable,
isInLink: selfIsLink || isInLink, isInLink: selfIsLink || isInLink,
isInTableHeaderRow: selfIsTableHeaderRow || isInTableHeaderRow, isInTableHeaderRow: selfIsTableHeaderRow || isInTableHeaderRow,
isInBlockquote: selfIsBlockquote || isInBlockquote,
isInList: selfIsList || isInList,
useMdx, useMdx,
shortcodeConfigs, shortcodeConfigs,
index: childIndex, index: childIndex,
@ -181,7 +189,25 @@ export default function deserializeMarkdown(node: MdastNode, options: Options) {
} as ListItemNode; } as ListItemNode;
case 'paragraph': case 'paragraph':
if ('ordered' in node) { if (isInBlockquote || isInList) {
if (isInBlockquote && index > 0) {
if (children.length > 0) {
let firstChild = children[0];
if ('text' in firstChild) {
firstChild = { text: `\n\n${firstChild.text}` };
}
if (children.length > 1) {
const [_, ...rest] = children;
return [firstChild, ...rest];
}
return [firstChild];
}
return children;
}
return children; return children;
} }
@ -212,7 +238,20 @@ export default function deserializeMarkdown(node: MdastNode, options: Options) {
} as ImageNode; } as ImageNode;
case 'blockquote': case 'blockquote':
return { type: NodeTypes.block_quote, children } as BlockQuoteNode; const blockquoteChildren = children.reduce((acc, n) => {
const lastNode = acc.length > 0 ? acc[acc.length - 1] : null;
if (lastNode && 'text' in lastNode && lastNode.text && 'text' in n && n.text) {
acc[acc.length - 1] = {
text: `${lastNode.text}${n.text}`,
};
} else {
acc.push(n);
}
return acc;
}, [] as DeserializedNode[]);
return { type: NodeTypes.block_quote, children: blockquoteChildren } as BlockQuoteNode;
case 'code': case 'code':
return { return {
@ -230,7 +269,7 @@ export default function deserializeMarkdown(node: MdastNode, options: Options) {
children: [{ text: node.value?.replace(/<br>/g, '') || '' }], children: [{ text: node.value?.replace(/<br>/g, '') || '' }],
} as ParagraphNode; } as ParagraphNode;
} }
return { type: 'p', children: [{ text: node.value || '' }] }; return { type: 'p', children: [{ text: node.value ?? '' }] };
case 'emphasis': case 'emphasis':
return { return {
@ -285,7 +324,7 @@ export default function deserializeMarkdown(node: MdastNode, options: Options) {
} }
} }
return { text: node.value || '' }; return { text: node.value ?? '' };
case 'mdxJsxTextElement': case 'mdxJsxTextElement':
if ('name' in node && node.type === 'mdxJsxTextElement') { if ('name' in node && node.type === 'mdxJsxTextElement') {
@ -344,26 +383,37 @@ export default function deserializeMarkdown(node: MdastNode, options: Options) {
} }
} }
return { text: node.value || '' }; return { text: node.value ?? '' };
case 'break':
return { text: '\n' };
case 'text': case 'text':
if (useMdx) { if (useMdx) {
return { text: node.value || '' }; return { text: (node.value ?? '').replace(/(?<![\n]|[ ]{2})[\n]{1}([^\n])/g, ' $1') };
} }
if (!node.value) { if (!node.value) {
return { text: '' }; return { text: '' };
} }
const nodes = autoLinkToSlate( let nodes = autoLinkToSlate(
processShortcodeConfigsToSlate(shortcodeConfigs, [node]), processShortcodeConfigsToSlate(shortcodeConfigs, [node]),
isInLink, isInLink,
); );
return nodes.map(node => (node.type === 'text' ? { text: node.value ?? '' } : node)); nodes = nodes.map(n => {
if (n.type !== 'text') {
return n;
}
return { text: (n.value ?? '').replace(/(?<![\n]|[ ]{2})[\n]{1}([^\n])/g, ' $1') };
});
return nodes;
default: default:
console.warn('[StaticCMS] Unrecognized mdast node, proceeding as text', node); console.warn('[StaticCMS] Unrecognized mdast node, proceeding as text', node);
return { text: node.value || '' }; return { text: node.value ?? '' };
} }
} }

View File

@ -75,14 +75,13 @@ const serializationTestData: SerializationTests = {
}, },
'paragraph with line break': { 'paragraph with line break': {
markdown: `A line of text markdown: `A line of text with another in the same paragraph`,
With another in the same paragraph`,
slate: [ slate: [
{ {
type: ELEMENT_PARAGRAPH, type: ELEMENT_PARAGRAPH,
children: [ children: [
{ {
text: 'A line of text\nWith another in the same paragraph', text: 'A line of text with another in the same paragraph',
}, },
], ],
}, },
@ -234,20 +233,6 @@ And a completely new paragraph`,
], ],
}, },
'multiline blockquote': {
markdown: '> I am a block quote\n> And another line',
slate: [
{
type: ELEMENT_BLOCKQUOTE,
children: [
{
text: 'I am a block quote\nAnd another line',
},
],
},
],
},
'nested blockquote': { 'nested blockquote': {
markdown: '> I am a block quote\n> > And another line', markdown: '> I am a block quote\n> > And another line',
slate: [ slate: [
@ -269,6 +254,116 @@ And a completely new paragraph`,
}, },
] as MdValue, ] as MdValue,
}, },
'multiline blockquote (double space and carrot each line)': {
markdown: '> One line \n> Another line',
slate: [
{
type: ELEMENT_BLOCKQUOTE,
children: [
{
text: 'One line\nAnother line',
},
],
},
] as MdValue,
},
'multiline blockquote (empty line carrot)': {
markdown: '> One line\n>\n> Another line',
slate: [
{
type: ELEMENT_BLOCKQUOTE,
children: [
{
text: 'One line\n\nAnother line',
},
],
},
] as MdValue,
},
'sequential blockquote': {
markdown: `> I am a block quote
> And another block quote`,
slate: [
{
type: ELEMENT_BLOCKQUOTE,
children: [
{
text: 'I am a block quote',
},
],
},
{
type: ELEMENT_BLOCKQUOTE,
children: [
{
text: 'And another block quote',
},
],
},
] as MdValue,
},
'blockquote with link': {
// First line has double space
markdown: `> I am a [block quote](https://example.com/). Another line
>
> Final Line`,
slate: [
{
type: ELEMENT_BLOCKQUOTE,
children: [
{
text: 'I am a ',
},
{
type: ELEMENT_LINK,
url: 'https://example.com/',
children: [
{
text: 'block quote',
},
],
},
{
text: '. Another line\n\nFinal Line',
},
],
},
],
},
'blockquote with link (no punctuation)': {
// First line has double space
markdown: `> I am a [block quote](https://example.com/) and another line
>
> Final Line`,
slate: [
{
type: ELEMENT_BLOCKQUOTE,
children: [
{
text: 'I am a ',
},
{
type: ELEMENT_LINK,
url: 'https://example.com/',
children: [
{
text: 'block quote',
},
],
},
{
text: ' and another line\n\nFinal Line',
},
],
},
],
},
}, },
}, },
@ -1543,8 +1638,7 @@ label: 'Blog post content',
widget: 'markdown', widget: 'markdown',
\`\`\` \`\`\`
> See the table below for default options > See the table below for default options \n> More API information can be found in the document
> More API information can be found in the document
|Name|Type|Default|Description| |Name|Type|Default|Description|
|---|---|---|---| |---|---|---|---|
@ -2683,8 +2777,86 @@ Text ahead [youtube|p6h-rYSVX90] and behind and another {{< twitter 917359331535
}; };
export const deserializationOnlyTestData: SerializationTests = { export const deserializationOnlyTestData: SerializationTests = {
blcokquote: {
both: {
'blockquote with link': {
// First line has double space
markdown: `> I am a [block quote](https://example.com/).
> Another line
>
> Final Line`,
slate: [
{
type: ELEMENT_BLOCKQUOTE,
children: [
{
text: 'I am a ',
},
{
type: ELEMENT_LINK,
url: 'https://example.com/',
children: [
{
text: 'block quote',
},
],
},
{
text: '. Another line\n\nFinal Line',
},
],
},
],
},
'multiline blockquote (carrot each line)': {
markdown: `> One line
> Another line`,
slate: [
{
type: ELEMENT_BLOCKQUOTE,
children: [
{
text: 'One line Another line',
},
],
},
] as MdValue,
},
'multiline blockquote (double space)': {
markdown: '> One line \nAnother line',
slate: [
{
type: ELEMENT_BLOCKQUOTE,
children: [
{
text: 'One line\nAnother line',
},
],
},
] as MdValue,
},
},
},
paragraph: { paragraph: {
markdown: { markdown: {
'paragraph with line break': {
markdown: `A line of text
With another in the same paragraph`,
slate: [
{
type: ELEMENT_PARAGRAPH,
children: [
{
text: 'A line of text With another in the same paragraph',
},
],
},
],
},
'paragraph with link': { 'paragraph with link': {
markdown: markdown:
'A line of text with a link https://www.youtube.com/watch?v=p6h-rYSVX90 and some more text', 'A line of text with a link https://www.youtube.com/watch?v=p6h-rYSVX90 and some more text',