1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-04-07 11:16:55 +00:00

feat: example ast round trip

This commit is contained in:
linonetwo 2025-03-24 00:38:03 +08:00
parent 2941bf6af3
commit e2414094f6
3 changed files with 269 additions and 102 deletions

View File

@ -1,40 +1,89 @@
/*\
title: $:/plugins/tiddlywiki/prosemirror/ast/wikiAstFromProsemirrorAst.js
title: $:/plugins/tiddlywiki/prosemirror/ast/from-prosemirror.js
type: application/javascript
module-type: library
Get the Prosemirror AST from a Wiki AST
Get the Wiki AST from a Prosemirror AST
\*/
function doc(context, node) {
return convertNodes(context, node.content);
}
function paragraph(context, node) {
return {
type: "element",
tag: "p",
rule: "parseblock",
children: convertNodes(context, node.content)
};
}
function text(context, node) {
return {
type: "text",
text: node.text
}
}
function heading(context, node) {
return {
type: "element",
tag: "h" + node.attrs.level,
rule: "heading",
attributes: {
// TODO: restore class if any
},
children: convertNodes(context, node.content)
};
}
function bullet_list(context, node) {
return {
type: "element",
tag: "ul",
rule: "list",
children: convertNodes(context, node.content)
};
}
function ordered_list(context, node) {
return {
type: "element",
tag: "ol",
rule: "list",
children: convertNodes(context, node.content)
};
}
function list_item(context, node) {
return {
type: "element",
tag: "li",
rule: "list",
children: convertNodes(context, node.content)
};
}
/**
* Key is `node.type`, value is node converter function.
*/
const builders = {
// auto parse basic element nodes
// eslint-disable-next-line unicorn/prefer-object-from-entries
...(htmlTags).reduce(
(previousValue, currentValue) => {
previousValue[currentValue] = element;
return previousValue;
},
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter, @typescript-eslint/consistent-type-assertions
{},
),
[ELEMENT_CODE_BLOCK]: codeblock,
[ELEMENT_LIC]: lic,
doc,
paragraph,
text,
widget,
macro: widget,
set,
heading,
bullet_list,
ordered_list,
list_item,
};
function wikiAstFromProsemirrorAst(input) {
function wikiAstFromProseMirrorAst(input) {
return convertNodes(builders, Array.isArray(input) ? input : [input]);
}
exports.wikiAstFromProsemirrorAst = wikiAstFromProsemirrorAst;
exports.from = wikiAstFromProseMirrorAst;
function convertNodes(builders, nodes) {
if (nodes === undefined || nodes.length === 0) {
@ -42,27 +91,22 @@ function convertNodes(builders, nodes) {
}
return nodes.reduce((accumulator, node) => {
return [...accumulator, ...convertWikiAstNode(builders, node)];
return [...accumulator, ...convertANode(builders, node)];
}, []);
}
function convertWikiAstNode(builders, node) {
// only text and root node don't have a `type` field, deal with it first
if (isText(node)) {
return [builders.text(builders, node)];
}
if (isElement(node)) {
const builder = builders[node.type];
if (typeof builder === 'function') {
const builtSlateNodeOrNodes = builder(builders, node);
return Array.isArray(builtSlateNodeOrNodes)
? builtSlateNodeOrNodes.map((child) => ({ ...getSlatePlateASTAdditionalProperties(node), ...child }))
: ([{ ...getSlatePlateASTAdditionalProperties(node), ...builtSlateNodeOrNodes }]);
}
}
// it might be a root or pure parent node, reduce it
if ('children' in node) {
return convertNodes(builders, node.children);
function restoreMetadata(node) {
// TODO: restore attributes, orderedAttributes, isBlock
return {};
}
function convertANode(builders, node) {
var builder = builders[node.type];
if (typeof builder === 'function') {
var convertedNode = builder(builders, node);
var arrayOfNodes = (Array.isArray(convertedNode)
? convertedNode : [convertedNode]);
return arrayOfNodes.map((child) => ({ ...restoreMetadata(node), ...child }));
}
console.warn(`WikiAst get Unknown node type: ${JSON.stringify(node)}`);
return [];
}

View File

@ -1,83 +1,189 @@
/*\
title: $:/plugins/tiddlywiki/prosemirror/ast/wikiAstToProsemirrorAst.js
title: $:/plugins/tiddlywiki/prosemirror/ast/to-prosemirror.js
type: application/javascript
module-type: library
Get the Prosemirror AST from a Wiki AST
\*/
function wikiAstToProsemirrorAst(node, options) {
return convertNodes({ ...initialContext, ...options }, Array.isArray(node) ? node : [node]);
/**
* 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) {
return {
type: "bullet_list",
content: convertNodes(context, node.children)
};
},
ol: function(context, node) {
return {
type: "ordered_list",
content: convertNodes(context, node.children)
};
},
li: function(context, node) {
// In ProseMirror, list items must contain block content (not bare text)
// TODO: find solution to https://discuss.prosemirror.net/t/removing-the-default-paragraph-p-inside-a-list-item-li/2745/17
const processedContent = convertNodes(context, node.children);
const wrappedContent = wrapTextNodesInParagraphs(context, processedContent);
return {
type: "list_item",
content: wrappedContent
};
}
};
/**
* Helper function to ensure text nodes in list items are wrapped in paragraphs
* ProseMirror requires list items to contain block content, not bare text
*/
function wrapTextNodesInParagraphs(context, nodes) {
if (!nodes || nodes.length === 0) {
return [];
}
const result = [];
let currentTextNodes = [];
function flushTextNodes() {
if (currentTextNodes.length > 0) {
result.push({
type: "paragraph",
content: currentTextNodes
});
currentTextNodes = [];
}
}
nodes.forEach(node => {
// If it's a text node, collect it
if (node.type === "text") {
currentTextNodes.push(node);
} else {
// If we encounter a non-text node, flush any collected text nodes
flushTextNodes();
// Add the non-text node as is
result.push(node);
}
});
// Flush any remaining text nodes
flushTextNodes();
return result;
}
exports.wikiAstToProsemirrorAst = wikiAstToProsemirrorAst;
function element(context, node) {
const builder = elementBuilders[node.tag];
if (builder) {
return builder(context, node);
} else {
console.warn(`Unknown element tag: ${node.tag}`);
return [];
}
}
const initialContext = {
builders,
marks: {},
function text(context, node) {
return {
type: "text",
text: node.text
};
}
/**
* Key is wikiAst node type, value is node converter function.
*/
const builders = {
element,
text
};
function wikiAstToProsemirrorAst(node, options) {
const context = { ...builders, ...options };
const result = convertNodes(context, Array.isArray(node) ? node : [node]);
// Wrap in a doc if needed
if (result.length > 0 && result[0].type !== "doc") {
return {
type: "doc",
content: result
};
}
return result;
}
exports.to = wikiAstToProsemirrorAst;
function convertNodes(context, nodes) {
if (nodes === undefined || nodes.length === 0) {
return [{ text: '' }];
return [];
}
return nodes.reduce((accumulator, node) => {
return [...accumulator, ...prosemirrorNode(context, node)];
return [...accumulator, ...convertANode(context, node)];
}, []);
}
function prosemirrorNode(context, node) {
const id = context.idCreator?.();
const withId = (nodeToAddId) => (id === undefined ? nodeToAddId : { ...nodeToAddId, id });
if ('rule' in node && node.rule !== undefined && node.rule in context.builders) {
const builder = context.builders[node.rule];
if (typeof builder === 'function') {
// basic elements
const builtProsemirrorNodeOrNodes = builder(context, node);
return Array.isArray(builtProsemirrorNodeOrNodes)
? builtProsemirrorNodeOrNodes.map((child) => withId(child))
: ([withId(builtProsemirrorNodeOrNodes)]);
}
} else if ('text' in node) {
// text node
return [withId({ text: node.text })];
} else {
console.warn(`WikiAst get Unknown node type: ${JSON.stringify(node)}`);
return [];
function convertANode(context, node) {
var builder = context[node.type];
if (typeof builder === 'function') {
var convertedNode = builder(context, node);
var arrayOfNodes = (Array.isArray(convertedNode)
? convertedNode : [convertedNode]);
return arrayOfNodes;
}
console.warn(`ProseMirror get Unknown node type: ${JSON.stringify(node)}`);
return [];
}
const builders = {
element,
text,
};
/** Slate node is compact, we need to filter out some keys from wikiast */
const textLevelKeysToOmit = ['type', 'start', 'end'];
function text(context, text) {
return {
text: '', // provides default text
...omit(text, textLevelKeysToOmit),
...context.marks,
};
}
const elementBuilders = { ul, ol: ul, li, ...marks };
function element(context, node) {
const { tag, children } = node;
if (typeof elementBuilders[tag] === 'function') {
return elementBuilders[tag](context, node);
}
const result = {
type: tag,
children: convertNodes(context, children),
};
if (node.rule) {
result.rule = node.rule;
}
return result;
}

View File

@ -9,6 +9,8 @@ module-type: library
var Widget = require("$:/core/modules/widgets/widget.js").widget;
var debounce = require("$:/core/modules/utils/debounce.js").debounce;
var wikiAstFromProseMirrorAst = require("$:/plugins/tiddlywiki/prosemirror/ast/from-prosemirror.js").from;
var wikiAstToProseMirrorAst = require("$:/plugins/tiddlywiki/prosemirror/ast/to-prosemirror.js").to;
var { EditorState } = require("prosemirror-state");
var { EditorView } = require("prosemirror-view");
@ -39,15 +41,26 @@ ProsemirrorWidget.prototype.render = function(parent,nextSibling) {
// Mix the nodes from prosemirror-schema-list into the basic schema to
// create a schema with list support.
const mySchema = new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
var mySchema = new Schema({
// nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
nodes: addListNodes(schema.spec.nodes, "block*", "block"),
marks: schema.spec.marks
})
var self = this;
var wikiAst = $tw.wiki.parseText(null, `* This is an unordered list
* It has two items
# This is a numbered list
## With a subitem
# And a third item`).tree;
var doc = wikiAstToProseMirrorAst(wikiAst);
// DEBUG: console doc
console.log(`initial doc`, doc);
this.view = new EditorView(container, {
state: EditorState.create({
doc: mySchema.node("doc", null, [mySchema.node("paragraph")]),
// doc: mySchema.node("doc", null, [mySchema.node("paragraph")]),
doc: mySchema.nodeFromJSON(doc),
plugins: exampleSetup({schema: mySchema})
}),
dispatchTransaction: function(transaction) {
@ -62,8 +75,12 @@ ProsemirrorWidget.prototype.render = function(parent,nextSibling) {
};
ProsemirrorWidget.prototype.saveEditorContent = function() {
const content = this.view.state.doc.toJSON();
console.log(JSON.stringify(content));
var content = this.view.state.doc.toJSON();
console.log(`ProseMirror: ${JSON.stringify(content)}`);
var wikiast = wikiAstFromProseMirrorAst(content);
console.log(`WikiAST: ${JSON.stringify(wikiast)}`);
var wikiText = $tw.utils.serializeParseTree(wikiast);
console.log(`WikiText: ${wikiText}`);
}
// Debounced save function for performance