mirror of https://github.com/buster-so/buster.git
toggle processing
This commit is contained in:
parent
f5966f83b3
commit
17e19adf4f
|
@ -11,6 +11,7 @@ const MarkdownPlugin = PlateMarkdownPlugin.configure({
|
||||||
callout: calloutSerializer,
|
callout: calloutSerializer,
|
||||||
metric: metricSerializer,
|
metric: metricSerializer,
|
||||||
toggle: toggleSerializer,
|
toggle: toggleSerializer,
|
||||||
|
details: toggleSerializer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -280,48 +280,6 @@ Here's an unordered list:
|
||||||
expect(firstElement.type).toBe('metric');
|
expect(firstElement.type).toBe('metric');
|
||||||
expect(firstElement.metricId).toBe('33af38a8-c40f-437d-98ed-1ec78ce35232');
|
expect(firstElement.metricId).toBe('33af38a8-c40f-437d-98ed-1ec78ce35232');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('toggle', async () => {
|
|
||||||
const markdown = `<toggle content="This is toggle content"></toggle>`;
|
|
||||||
const elements = await markdownToPlatejs(editor, markdown);
|
|
||||||
const firstElement = elements[0];
|
|
||||||
expect(firstElement.type).toBe('toggle');
|
|
||||||
expect(firstElement.children[0]).toEqual({ text: '' });
|
|
||||||
|
|
||||||
expect(elements[1].type).toBe('p');
|
|
||||||
expect((elements[1] as any).indent).toBe(1);
|
|
||||||
expect(elements[1].children[0].text).toBe('This is toggle content');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('toggle with indent attributes and roundtrip', async () => {
|
|
||||||
const originalElements = [
|
|
||||||
{
|
|
||||||
type: 'toggle',
|
|
||||||
children: [{ text: 'This is a test:)' }],
|
|
||||||
id: 'hCbXepPz5W'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'p',
|
|
||||||
children: [{ text: 'WOW!' }],
|
|
||||||
id: 'Q-d_WjfXSV',
|
|
||||||
indent: 1
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const markdown = await platejsToMarkdown(editor, originalElements);
|
|
||||||
expect(markdown).toContain('<toggle ');
|
|
||||||
expect(markdown).toContain('content="');
|
|
||||||
expect(markdown).toContain('WOW!');
|
|
||||||
|
|
||||||
const elements = await markdownToPlatejs(editor, markdown);
|
|
||||||
|
|
||||||
expect(elements[0].type).toBe('toggle');
|
|
||||||
expect(elements[0].children[0]).toEqual({ text: '' });
|
|
||||||
|
|
||||||
expect(elements[1].type).toBe('p');
|
|
||||||
expect((elements[1] as any).indent).toBe(1);
|
|
||||||
expect(elements[1].children[0].text).toBe('WOW!');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('platejsToMarkdown', () => {
|
describe('platejsToMarkdown', () => {
|
||||||
|
@ -1538,3 +1496,23 @@ describe('platejs to markdown and back to platejs', () => {
|
||||||
expect(actualWithoutIds).toEqual(expectedWithoutIds);
|
expect(actualWithoutIds).toEqual(expectedWithoutIds);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('toggle serializer', () => {
|
||||||
|
it('should serialize a toggle', async () => {
|
||||||
|
const markdown = `<details>
|
||||||
|
<summary>Toggle</summary>
|
||||||
|
|
||||||
|
|
||||||
|
Nested
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</details>`;
|
||||||
|
const platejs = await markdownToPlatejs(editor, markdown);
|
||||||
|
expect(platejs).toBeDefined();
|
||||||
|
expect(platejs[0].type).toBe('toggle');
|
||||||
|
expect(platejs[0].children[0].text).toBe('Toggle');
|
||||||
|
expect(platejs[1].type).toBe('p');
|
||||||
|
expect(platejs[1].children[0].text).toBe('Nested');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Descendant, Value } from 'platejs';
|
import type { Descendant, TElement, Value } from 'platejs';
|
||||||
import type { IReportEditor } from '../../ReportEditor';
|
import type { IReportEditor } from '../../ReportEditor';
|
||||||
|
import { postProcessToggleDeserialization, postProcessToggleMarkdown } from './toggle-serializer';
|
||||||
|
|
||||||
export const markdownToPlatejs = async (
|
export const markdownToPlatejs = async (
|
||||||
editor: IReportEditor,
|
editor: IReportEditor,
|
||||||
|
@ -11,7 +12,11 @@ export const markdownToPlatejs = async (
|
||||||
...element,
|
...element,
|
||||||
id: `id-${index}`,
|
id: `id-${index}`,
|
||||||
}));
|
}));
|
||||||
return descendantsWithIds;
|
|
||||||
|
// Apply post-processing to handle details elements
|
||||||
|
const processedElements = postProcessToggleDeserialization(descendantsWithIds as TElement[]);
|
||||||
|
|
||||||
|
return processedElements as Value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error converting markdown to PlateJS:', error);
|
console.error('Error converting markdown to PlateJS:', error);
|
||||||
return [];
|
return [];
|
||||||
|
@ -22,5 +27,8 @@ export const platejsToMarkdown = async (
|
||||||
editor: IReportEditor,
|
editor: IReportEditor,
|
||||||
elements: Value
|
elements: Value
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
return editor.api.markdown.serialize({ value: elements as Descendant[] });
|
const markdown = editor.api.markdown.serialize({ value: elements as Descendant[] });
|
||||||
|
|
||||||
|
// Apply post-processing to handle toggle serialization
|
||||||
|
return postProcessToggleMarkdown(markdown);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,30 @@
|
||||||
import { deserializeMd, type MdNodeParser, parseAttributes, serializeMd } from '@platejs/markdown';
|
import { convertChildrenDeserialize, type MdNodeParser, serializeMd } from '@platejs/markdown';
|
||||||
|
import type { Descendant, TElement, Value } from 'platejs';
|
||||||
|
|
||||||
|
interface ToggleNode extends TElement {
|
||||||
|
type: 'toggle';
|
||||||
|
id?: string;
|
||||||
|
children: Descendant[];
|
||||||
|
_tempContent?: MdastNode[]; // Temporary storage for content paragraphs
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndentedNode extends TElement {
|
||||||
|
indent?: number;
|
||||||
|
id?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MdastNode {
|
||||||
|
type: string;
|
||||||
|
name?: string;
|
||||||
|
tagName?: string;
|
||||||
|
children?: MdastNode[];
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOGGLE_POST_PROCESSING_KEY = 'toggle-post-processing';
|
||||||
|
const TOGGLE_POST_PROCESSING_MATCH = /<toggle-post-processing>(.*?)<\/toggle-post-processing>/;
|
||||||
|
|
||||||
export const toggleSerializer: MdNodeParser<'toggle'> = {
|
export const toggleSerializer: MdNodeParser<'toggle'> = {
|
||||||
serialize: (node, options) => {
|
serialize: (node, options) => {
|
||||||
|
@ -6,98 +32,207 @@ export const toggleSerializer: MdNodeParser<'toggle'> = {
|
||||||
throw new Error('Editor is required');
|
throw new Error('Editor is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = (options as any).value ?? options.editor.children;
|
// Get the toggle title from the node's children (the summary text)
|
||||||
const nodeIndex = doc.indexOf(node as any);
|
const titleContent = serializeMd(options.editor, {
|
||||||
const contentNodes: any[] = [];
|
|
||||||
|
|
||||||
for (let i = nodeIndex + 1; i < doc.length; i++) {
|
|
||||||
const sibling = doc[i] as any;
|
|
||||||
const siblingIndent = sibling?.indent ?? 0;
|
|
||||||
|
|
||||||
if (siblingIndent >= 1) {
|
|
||||||
contentNodes.push(sibling);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = serializeMd(options.editor, {
|
|
||||||
...options,
|
...options,
|
||||||
value: contentNodes.length ? contentNodes : [{ type: 'p', children: [{ text: '' }] }],
|
value: node.children,
|
||||||
});
|
}).trim();
|
||||||
|
|
||||||
const attrs: string[] = [`content="${content}"`];
|
|
||||||
|
|
||||||
if ((node as any).id) {
|
|
||||||
attrs.push(`id="${(node as any).id}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((node as any).indent !== undefined && (node as any).indent !== null) {
|
|
||||||
attrs.push(`indent="${(node as any).indent}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Serialize to a special marker that we'll post-process
|
||||||
return {
|
return {
|
||||||
type: 'html',
|
type: 'html',
|
||||||
value: `<toggle ${attrs.join(' ')}></toggle>`,
|
value: `<${TOGGLE_POST_PROCESSING_KEY}>${titleContent}</${TOGGLE_POST_PROCESSING_KEY}>`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
deserialize: (node, _, options) => {
|
|
||||||
const typedAttributes = parseAttributes(node.attributes) as {
|
|
||||||
content: string;
|
|
||||||
id?: string;
|
|
||||||
indent?: string | number;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
deserialize: (node, _, options) => {
|
||||||
if (!options.editor) {
|
if (!options.editor) {
|
||||||
throw new Error('Editor is required');
|
throw new Error('Editor is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Parse details/summary structure
|
||||||
const deserializedContent = deserializeMd(options.editor, typedAttributes.content);
|
const mdastNode = node;
|
||||||
|
|
||||||
const toggleNode: any = {
|
if (mdastNode.name === 'details') {
|
||||||
|
// Find summary content and collect other content
|
||||||
|
let summaryChildren: Descendant[] = [];
|
||||||
|
const contentParagraphs: MdastNode[] = [];
|
||||||
|
|
||||||
|
if (mdastNode.children) {
|
||||||
|
for (const child of mdastNode.children) {
|
||||||
|
// Check if this paragraph contains a summary element
|
||||||
|
if (child.type === 'paragraph' && child.children) {
|
||||||
|
const summaryElement = child.children?.find(
|
||||||
|
(grandchild: MdastNode) =>
|
||||||
|
grandchild.type === 'mdxJsxTextElement' && grandchild.name === 'summary'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (summaryElement?.children) {
|
||||||
|
// Extract summary content preserving formatting
|
||||||
|
try {
|
||||||
|
summaryChildren = convertChildrenDeserialize(summaryElement.children, {}, options);
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to simple text extraction
|
||||||
|
summaryChildren = [{ text: extractTextFromNode(summaryElement) }];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is content - store it for post-processing
|
||||||
|
contentParagraphs.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the toggle element with temporary content storage
|
||||||
|
const toggleElement: ToggleNode = {
|
||||||
type: 'toggle',
|
type: 'toggle',
|
||||||
children: [{ text: '' }],
|
children: summaryChildren.length > 0 ? summaryChildren : [{ text: '' }],
|
||||||
|
id: generateId(),
|
||||||
|
_tempContent: contentParagraphs, // Store content for post-processing
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typedAttributes.id) {
|
console.log('DEBUG: Created toggle element:', toggleElement);
|
||||||
toggleNode.id = typedAttributes.id;
|
return toggleElement;
|
||||||
}
|
|
||||||
|
|
||||||
if (typedAttributes.indent !== undefined && typedAttributes.indent !== null) {
|
|
||||||
toggleNode.indent = typeof typedAttributes.indent === 'string'
|
|
||||||
? parseInt(typedAttributes.indent, 10)
|
|
||||||
: typedAttributes.indent;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentNodes = Array.isArray(deserializedContent) ? deserializedContent : [deserializedContent];
|
|
||||||
const processedContentNodes = contentNodes.map((contentNode: any) => {
|
|
||||||
const currentIndent = contentNode.indent ?? 0;
|
|
||||||
return {
|
|
||||||
...contentNode,
|
|
||||||
indent: currentIndent >= 1 ? currentIndent : 1,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return [toggleNode, ...processedContentNodes] as any;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deserializing content', error);
|
|
||||||
const result: any = {
|
|
||||||
type: 'toggle',
|
|
||||||
children: [{ text: typedAttributes.content }],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typedAttributes.id) {
|
|
||||||
result.id = typedAttributes.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typedAttributes.indent !== undefined && typedAttributes.indent !== null) {
|
|
||||||
result.indent = typeof typedAttributes.indent === 'string'
|
|
||||||
? parseInt(typedAttributes.indent, 10)
|
|
||||||
: typedAttributes.indent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback for other node types
|
||||||
|
return {
|
||||||
|
type: 'toggle',
|
||||||
|
children: [{ text: '' }],
|
||||||
|
id: generateId(),
|
||||||
|
} as ToggleNode;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to extract text content from a node
|
||||||
|
function extractTextFromNode(node: MdastNode): string {
|
||||||
|
if (node.type === 'text') {
|
||||||
|
return node.value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children) {
|
||||||
|
return node.children.map(extractTextFromNode).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple ID generator
|
||||||
|
function generateId(): string {
|
||||||
|
return Math.random().toString(36).substr(2, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-processing function to handle details elements after deserialization
|
||||||
|
export function postProcessToggleDeserialization(elements: TElement[]): Value {
|
||||||
|
const result: Value = [];
|
||||||
|
|
||||||
|
for (const element of elements) {
|
||||||
|
// Check if this is a toggle element with temporary content
|
||||||
|
const toggleElement = element as ToggleNode;
|
||||||
|
if (toggleElement.type === 'toggle' && toggleElement._tempContent) {
|
||||||
|
// Convert the stored content paragraphs to indented elements
|
||||||
|
const contentElements: TElement[] = [];
|
||||||
|
|
||||||
|
for (const contentParagraph of toggleElement._tempContent) {
|
||||||
|
if (contentParagraph.type === 'paragraph' && contentParagraph.children) {
|
||||||
|
const paragraphText = extractTextFromNode(contentParagraph);
|
||||||
|
if (paragraphText.trim()) {
|
||||||
|
contentElements.push({
|
||||||
|
type: 'p',
|
||||||
|
children: [{ text: paragraphText.trim() }],
|
||||||
|
indent: 1,
|
||||||
|
id: generateId(),
|
||||||
|
} as TElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the toggle element (remove temp content)
|
||||||
|
const cleanToggleElement: ToggleNode = {
|
||||||
|
type: 'toggle',
|
||||||
|
children: toggleElement.children,
|
||||||
|
id: toggleElement.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add toggle followed by indented content
|
||||||
|
result.push(cleanToggleElement);
|
||||||
|
result.push(...contentElements);
|
||||||
|
} else {
|
||||||
|
// Keep other elements as-is
|
||||||
|
result.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-processing function to convert toggle markers to details/summary format
|
||||||
|
export function postProcessToggleMarkdown(markdown: string): string {
|
||||||
|
// This approach looks for toggle-post-processing markers followed by indented content
|
||||||
|
// and groups them into details/summary blocks
|
||||||
|
|
||||||
|
const lines = markdown.split('\n');
|
||||||
|
const processedLines: string[] = [];
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i];
|
||||||
|
|
||||||
|
// Check if this line contains a toggle-post-processing marker
|
||||||
|
const toggleMatch = line.match(TOGGLE_POST_PROCESSING_MATCH);
|
||||||
|
|
||||||
|
if (toggleMatch) {
|
||||||
|
const title = toggleMatch[1];
|
||||||
|
|
||||||
|
// Start the details block
|
||||||
|
processedLines.push('<details>');
|
||||||
|
processedLines.push(`<summary>${title}</summary>`);
|
||||||
|
processedLines.push(''); // Empty line after summary
|
||||||
|
|
||||||
|
i++; // Move to next line
|
||||||
|
|
||||||
|
// Simple approach: collect everything until we hit another toggle or end of document
|
||||||
|
const toggleContent: string[] = [];
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
const nextLine = lines[i];
|
||||||
|
|
||||||
|
// Stop if we hit another toggle marker
|
||||||
|
if (nextLine.includes(`<${TOGGLE_POST_PROCESSING_KEY}>`)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this line to toggle content
|
||||||
|
toggleContent.push(nextLine);
|
||||||
|
i++;
|
||||||
|
|
||||||
|
// Simple heuristic: if we hit two consecutive empty lines after some content, stop
|
||||||
|
if (
|
||||||
|
toggleContent.length > 1 &&
|
||||||
|
nextLine.trim() === '' &&
|
||||||
|
i < lines.length &&
|
||||||
|
lines[i]?.trim() === ''
|
||||||
|
) {
|
||||||
|
// Found two empty lines - this suggests end of toggle content
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the collected content
|
||||||
|
processedLines.push(...toggleContent);
|
||||||
|
|
||||||
|
// Close the details block
|
||||||
|
processedLines.push('</details>');
|
||||||
|
|
||||||
|
// Add spacing after details if there's more content
|
||||||
|
if (i < lines.length) {
|
||||||
|
processedLines.push('');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
processedLines.push(line);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedLines.join('\n');
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue