From dbe1c20c290a08d8ce8ee33e07fe36f7cd5a14f6 Mon Sep 17 00:00:00 2001 From: lin onetwo Date: Tue, 25 Mar 2025 23:55:52 +0800 Subject: [PATCH] feat: adapt prosemirror-flat-list --- .../prosemirror/ast/from-prosemirror.js | 70 +++++++--- .../prosemirror/ast/to-prosemirror.js | 121 ++++++++---------- plugins/tiddlywiki/prosemirror/readme.tid | 2 + plugins/tiddlywiki/prosemirror/widget.js | 2 + 4 files changed, 111 insertions(+), 84 deletions(-) diff --git a/plugins/tiddlywiki/prosemirror/ast/from-prosemirror.js b/plugins/tiddlywiki/prosemirror/ast/from-prosemirror.js index 9cc969a96..3521a2e0c 100644 --- a/plugins/tiddlywiki/prosemirror/ast/from-prosemirror.js +++ b/plugins/tiddlywiki/prosemirror/ast/from-prosemirror.js @@ -7,27 +7,27 @@ Get the Wiki AST from a Prosemirror AST \*/ -function doc(context, node) { - return convertNodes(context, node.content); +function doc(builder, node) { + return convertNodes(builder, node.content); } -function paragraph(context, node) { +function paragraph(builder, node) { return { type: "element", tag: "p", rule: "parseblock", - children: convertNodes(context, node.content) + children: convertNodes(builder, node.content) }; } -function text(context, node) { +function text(builder, node) { return { type: "text", text: node.text } } -function heading(context, node) { +function heading(builder, node) { return { type: "element", tag: "h" + node.attrs.level, @@ -35,21 +35,50 @@ function heading(context, node) { attributes: { // TODO: restore class if any }, - children: convertNodes(context, node.content) + children: convertNodes(builder, node.content) }; } -function list(context, node) { +function list(builder, node, context) { const listType = node.attrs && node.attrs.kind === "ordered" ? "ol" : "ul"; - const listItems = node.content.map(item => { - return { + // Prepare an array to store all list items + let listItems = []; + + // Add content from current node to list items + node.content.forEach(item => { + listItems.push({ type: "element", tag: "li", - children: convertANode(context, item) - }; + children: convertANode(builder, item) + }); }); + // Check if there are adjacent lists of the same type + while (context && context.nodes && context.nodes.length > 0) { + const nextNode = context.nodes[0]; + + // If next node is also a list of the same type + if (nextNode.type === 'list' && + ((node.attrs && node.attrs.kind) === (nextNode.attrs && nextNode.attrs.kind))) { + + // Remove and consume the next node + const consumedNode = context.nodes.shift(); + + // Merge its content into current list + consumedNode.content.forEach(item => { + listItems.push({ + type: "element", + tag: "li", + children: convertANode(builder, item) + }); + }); + } else { + // If next node is not a list of the same type, stop merging + break; + } + } + return { type: "element", tag: listType, @@ -80,19 +109,26 @@ function convertNodes(builders, nodes) { return []; } - return nodes.reduce((accumulator, node) => { - return [...accumulator, ...convertANode(builders, node)]; - }, []); + const result = []; + const nodesCopy = [...nodes]; // Create a copy to avoid modifying the original array + + while (nodesCopy.length > 0) { + const node = nodesCopy.shift(); // Get and remove the first node + const convertedNodes = convertANode(builders, node, { nodes: nodesCopy }); + result.push(...convertedNodes); + } + + return result; } function restoreMetadata(node) { // TODO: restore attributes, orderedAttributes, isBlock return {}; } -function convertANode(builders, node) { +function convertANode(builders, node, context) { var builder = builders[node.type]; if (typeof builder === 'function') { - var convertedNode = builder(builders, node); + var convertedNode = builder(builders, node, context); var arrayOfNodes = (Array.isArray(convertedNode) ? convertedNode : [convertedNode]); return arrayOfNodes.map((child) => ({ ...restoreMetadata(node), ...child })); diff --git a/plugins/tiddlywiki/prosemirror/ast/to-prosemirror.js b/plugins/tiddlywiki/prosemirror/ast/to-prosemirror.js index aac4117b3..3be1ea5a4 100644 --- a/plugins/tiddlywiki/prosemirror/ast/to-prosemirror.js +++ b/plugins/tiddlywiki/prosemirror/ast/to-prosemirror.js @@ -7,59 +7,25 @@ Get the Prosemirror AST from a Wiki AST \*/ -/** - * Many node shares same type `element` in wikiAst, we need to distinguish them by tag. - */ -const elementBuilders = { - p: function(context, node) { - return { - type: "paragraph", - content: convertNodes(context, node.children) - }; - }, - h1: function(context, node) { - return { - type: "heading", - attrs: { level: 1 }, - content: convertNodes(context, node.children) - }; - }, - h2: function(context, node) { - return { - type: "heading", - attrs: { level: 2 }, - content: convertNodes(context, node.children) - }; - }, - h3: function(context, node) { - return { - type: "heading", - attrs: { level: 3 }, - content: convertNodes(context, node.children) - }; - }, - h4: function(context, node) { - return { - type: "heading", - attrs: { level: 4 }, - content: convertNodes(context, node.children) - }; - }, - h5: function(context, node) { - return { - type: "heading", - attrs: { level: 5 }, - content: convertNodes(context, node.children) - }; - }, - h6: function(context, node) { - return { - type: "heading", - attrs: { level: 6 }, - content: convertNodes(context, node.children) - }; - }, - ul: function(context, node) { +function buildParagraph(context, node) { + return { + type: "paragraph", + content: convertNodes(context, node.children) + }; +} + +function buildHeading(context, node, level) { + return { + type: "heading", + attrs: { level: level }, + content: convertNodes(context, node.children) + }; +} + +function buildUnorderedList(context, node) { + // Prosemirror requires split all lists into separate lists with single items + return node.children.map(item => { + const processedItem = convertANode({...context, level: context.level + 1}, item); return { type: "list", attrs: { @@ -68,10 +34,14 @@ const elementBuilders = { checked: false, collapsed: false }, - content: convertNodes(context, node.children) + content: processedItem }; - }, - ol: function(context, node) { + }); +} + +function buildOrderedList(context, node) { + return node.children.map(item => { + const processedItem = convertANode({...context, level: context.level + 1}, item); return { type: "list", attrs: { @@ -80,17 +50,11 @@ const elementBuilders = { checked: false, collapsed: false }, - content: convertNodes(context, node.children) + content: processedItem }; - }, - li: function(context, node) { - // In ProseMirror, list items are converted to paragraphs or other block content - // directly under the list node, no special list_item type needed - const processedContent = convertNodes(context, node.children); - // Ensure content starts with a block element (typically paragraph) - return wrapTextNodesInParagraphs(context, processedContent); - } -}; + }); +} + /** * Helper function to ensure text nodes in list items are wrapped in paragraphs @@ -132,6 +96,28 @@ function wrapTextNodesInParagraphs(context, nodes) { return result; } +function buildListItem(context, node) { + const processedContent = convertNodes({...context, level: context.level + 1}, node.children); + // Ensure content starts with a block element (typically paragraph) + return wrapTextNodesInParagraphs(context, processedContent); +} + +/** + * Many node shares same type `element` in wikiAst, we need to distinguish them by tag. + */ +const elementBuilders = { + p: buildParagraph, + h1: (context, node) => buildHeading(context, node, 1), + h2: (context, node) => buildHeading(context, node, 2), + h3: (context, node) => buildHeading(context, node, 3), + h4: (context, node) => buildHeading(context, node, 4), + h5: (context, node) => buildHeading(context, node, 5), + h6: (context, node) => buildHeading(context, node, 6), + ul: buildUnorderedList, + ol: buildOrderedList, + li: buildListItem +}; + function element(context, node) { const builder = elementBuilders[node.tag]; if (builder) { @@ -158,7 +144,8 @@ const builders = { }; function wikiAstToProsemirrorAst(node, options) { - const context = { ...builders, ...options }; + // Initialize context with level tracking + const context = { ...builders, ...options, level: 0 }; const result = convertNodes(context, Array.isArray(node) ? node : [node]); // Wrap in a doc if needed diff --git a/plugins/tiddlywiki/prosemirror/readme.tid b/plugins/tiddlywiki/prosemirror/readme.tid index 15d5db7af..e0c23e05d 100755 --- a/plugins/tiddlywiki/prosemirror/readme.tid +++ b/plugins/tiddlywiki/prosemirror/readme.tid @@ -5,3 +5,5 @@ Test `<$prosemirror />` widget below, see console for content JSON. <$edit-prosemirror tiddler="$:/plugins/tiddlywiki/prosemirror/example"/> <$edit-text tiddler="$:/plugins/tiddlywiki/prosemirror/example" class="tc-edit-texteditor"/> + +{{$:/plugins/tiddlywiki/prosemirror/example}} diff --git a/plugins/tiddlywiki/prosemirror/widget.js b/plugins/tiddlywiki/prosemirror/widget.js index 3ca27809b..3edd8a560 100644 --- a/plugins/tiddlywiki/prosemirror/widget.js +++ b/plugins/tiddlywiki/prosemirror/widget.js @@ -46,6 +46,8 @@ ProsemirrorWidget.prototype.render = function(parent,nextSibling) { var tiddler = this.getAttribute("tiddler"); var initialText = this.wiki.getTiddlerText(tiddler, ""); var initialWikiAst = $tw.wiki.parseText(null, initialText).tree; + // DEBUG: console initialWikiAst + console.log(`initialWikiAst`, initialWikiAst); var doc = wikiAstToProseMirrorAst(initialWikiAst); // DEBUG: console doc console.log(`initial doc`, doc);