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:
parent
2941bf6af3
commit
e2414094f6
@ -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 [];
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user