Added jsdoc types for prosemirror

Also added link markdown handling when target is set.
This commit is contained in:
Dan Brown 2022-01-16 15:21:57 +00:00
parent 89194a3f85
commit 7622106665
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
9 changed files with 198 additions and 17 deletions

6
TODO
View file

@ -30,3 +30,9 @@
- Remove links button? (Action already in place if link href is empty). - Remove links button? (Action already in place if link href is empty).
- Links - Limit target attribute options and validate URL. - Links - Limit target attribute options and validate URL.
- Links - Integrate entity picker. - Links - Integrate entity picker.
### Notes
- Use NodeViews for embedded content (Code, Drawings) where control is needed.
- Probably still easiest to have seperate (codemirror) MD editor. Can alter display output via NodeViews to make MD like
but its tricky since editing the markdown content would change the block definition/type while editing.

View file

@ -1,3 +1,8 @@
/**
* @param {String} attrName
* @param {String} attrValue
* @return {PmCommandHandler}
*/
export function setBlockAttr(attrName, attrValue) { export function setBlockAttr(attrName, attrValue) {
return function (state, dispatch) { return function (state, dispatch) {
const ref = state.selection; const ref = state.selection;
@ -37,6 +42,10 @@ export function setBlockAttr(attrName, attrValue) {
} }
} }
/**
* @param {PmNodeType} blockType
* @return {PmCommandHandler}
*/
export function insertBlockBefore(blockType) { export function insertBlockBefore(blockType) {
return function (state, dispatch) { return function (state, dispatch) {
const startPosition = state.selection.$from.before(1); const startPosition = state.selection.$from.before(1);
@ -49,6 +58,9 @@ export function insertBlockBefore(blockType) {
} }
} }
/**
* @return {PmCommandHandler}
*/
export function removeMarks() { export function removeMarks() {
return function (state, dispatch) { return function (state, dispatch) {
if (dispatch) { if (dispatch) {

View file

@ -48,6 +48,10 @@ parser.tokenHandlers.html_inline = function(state, tok, tokens, i) {
} }
} }
/**
* @param {String} html
* @return {PmMark[]}
*/
function extractMarksFromHtml(html) { function extractMarksFromHtml(html) {
const contentDoc = htmlToDoc('<p>' + (html || '') + '</p>'); const contentDoc = htmlToDoc('<p>' + (html || '') + '</p>');
const marks = contentDoc?.content?.content?.[0]?.content?.content?.[0]?.marks; const marks = contentDoc?.content?.content?.[0]?.content?.content?.[0]?.marks;

View file

@ -1,14 +1,45 @@
import {MarkdownSerializer, defaultMarkdownSerializer} from "prosemirror-markdown"; import {MarkdownSerializer, defaultMarkdownSerializer, MarkdownSerializerState} from "prosemirror-markdown";
import {docToHtml} from "./util"; import {docToHtml} from "./util";
const nodes = defaultMarkdownSerializer.nodes; const nodes = defaultMarkdownSerializer.nodes;
const marks = defaultMarkdownSerializer.marks; const marks = defaultMarkdownSerializer.marks;
nodes.callout = function(state, node) { nodes.callout = function (state, node) {
writeNodeAsHtml(state, node); writeNodeAsHtml(state, node);
}; };
function isPlainURL(link, parent, index, side) {
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) {
return false
}
const content = parent.child(index + (side < 0 ? -1 : 0));
if (!content.isText || content.text != link.attrs.href || content.marks[content.marks.length - 1] != link) {
return false
}
if (index == (side < 0 ? 1 : parent.childCount - 1)) {
return true
}
const next = parent.child(index + (side < 0 ? -2 : 1));
return !link.isInSet(next.marks)
}
marks.link = {
open(state, mark, parent, index) {
const attrs = mark.attrs;
if (attrs.target) {
return `<a href="${attrs.target}" ${attrs.title ? `title="${attrs.title}"` : ''} target="${attrs.target}">`
}
return isPlainURL(mark, parent, index, 1) ? "<" : "["
},
close(state, mark, parent, index) {
if (mark.attrs.target) {
return `</a>`;
}
return isPlainURL(mark, parent, index, -1) ? ">"
: "](" + state.esc(mark.attrs.href) + (mark.attrs.title ? " " + state.quote(mark.attrs.title) : "") + ")"
}
};
marks.underline = { marks.underline = {
open: '<span style="text-decoration: underline;">', open: '<span style="text-decoration: underline;">',
@ -44,9 +75,12 @@ marks.background_color = {
close: '</span>', close: '</span>',
}; };
/**
* @param {MarkdownSerializerState} state
* @param node
*/
function writeNodeAsHtml(state, node) { function writeNodeAsHtml(state, node) {
const html = docToHtml({ content: [node] }); const html = docToHtml({content: [node]});
state.write(html); state.write(html);
state.ensureNewLine(); state.ensureNewLine();
state.write('\n'); state.write('\n');
@ -57,7 +91,7 @@ function writeNodeAsHtml(state, node) {
// or element that cannot be represented in commonmark without losing // or element that cannot be represented in commonmark without losing
// formatting or content. // formatting or content.
for (const [nodeType, serializerFunction] of Object.entries(nodes)) { for (const [nodeType, serializerFunction] of Object.entries(nodes)) {
nodes[nodeType] = function(state, node, parent, index) { nodes[nodeType] = function (state, node, parent, index) {
if (node.attrs.align) { if (node.attrs.align) {
writeNodeAsHtml(state, node); writeNodeAsHtml(state, node);
} else { } else {

View file

@ -6,7 +6,11 @@ import schema from "../schema";
import {MenuItem} from "./menu"; import {MenuItem} from "./menu";
import {icons} from "./icons"; import {icons} from "./icons";
/**
* @param {PmMarkType} markType
* @param {String} attribute
* @return {(function(PmEditorState): (string|null))}
*/
function getMarkAttribute(markType, attribute) { function getMarkAttribute(markType, attribute) {
return function (state) { return function (state) {
const marks = state.selection.$head.marks(); const marks = state.selection.$head.marks();
@ -20,6 +24,11 @@ function getMarkAttribute(markType, attribute) {
}; };
} }
/**
* @param {(function(FormData))} submitter
* @param {Function} closer
* @return {DialogBox}
*/
function getLinkDialog(submitter, closer) { function getLinkDialog(submitter, closer) {
return new DialogBox([ return new DialogBox([
new DialogForm([ new DialogForm([
@ -64,6 +73,12 @@ function applyLink(formData, state, dispatch) {
return true; return true;
} }
/**
* @param {PmEditorState} state
* @param {PmDispatchFunction} dispatch
* @param {PmView} view
* @param {Event} e
*/
function onPress(state, dispatch, view, e) { function onPress(state, dispatch, view, e) {
const dialog = getLinkDialog((data) => { const dialog = getLinkDialog((data) => {
applyLink(data, state, dispatch); applyLink(data, state, dispatch);
@ -77,6 +92,9 @@ function onPress(state, dispatch, view, e) {
document.body.appendChild(dom); document.body.appendChild(dom);
} }
/**
* @return {MenuItem}
*/
function anchorButtonItem() { function anchorButtonItem() {
return new MenuItem({ return new MenuItem({
title: "Insert/Edit Anchor Link", title: "Insert/Edit Anchor Link",

View file

@ -1,7 +0,0 @@
- Use NodeViews for embedded content (Code, Drawings) where control is needed.
- Probably still easiest to have seperate (codemirror) MD editor. Can alter display output via NodeViews to make MD like
but its tricky since editing the markdown content would change the block definition/type while editing.
-

View file

@ -3,9 +3,10 @@ import {Schema} from "prosemirror-model";
import nodes from "./schema-nodes"; import nodes from "./schema-nodes";
import marks from "./schema-marks"; import marks from "./schema-marks";
const index = new Schema({ /** @var {PmSchema} schema */
const schema = new Schema({
nodes, nodes,
marks, marks,
}); });
export default index; export default schema;

View file

@ -0,0 +1,106 @@
/**
* @typedef {Object} PmEditorState
* @property {PmNode} doc
* @property {PmSelection} selection
* @property {PmMark[]|null} storedMarks
* @property {PmSchema} schema
* @property {PmTransaction} tr
*/
/**
* @typedef {Object} PmNode
* @property {PmNodeType} type
* @property {Object} attrs
* @property {PmFragment} content
* @property {PmMark[]} marks
* @property {String|null} text
* @property {Number} nodeSize
* @property {Number} childCount
*/
/**
* @typedef {Object} PmNodeType
*/
/**
* @typedef {Object} PmMark
* @property {PmMarkType} type
* @property {Object} attrs
*/
/**
* @typedef {Object} PmMarkType
* @property {String} name
* @property {PmSchema} schema
* @property {PmMarkSpec} spec
*/
/**
* @typedef {Object} PmMarkSpec
*/
/**
* @typedef {Object} PmSchema
* @property {PmSchema} schema
* @property {Object<PmNodeType>} nodes
* @property {Object<PmMarkType>} marks
* @property {PmNodeType} topNodeType
* @property {Object} cached
*/
/**
* @typedef {Object} PmSelection
* @property {PmSelectionRange[]} ranges
* @property {PmResolvedPos} $anchor
* @property {PmResolvedPos} $head
* @property {Number} anchor
* @property {Number} head
* @property {Number} from
* @property {Number} to
* @property {PmResolvedPos} $from
* @property {PmResolvedPos} $to
* @property {Boolean} empty
*/
/**
* @typedef {Object} PmResolvedPos
* @property {Number} pos
* @property {Number} depth
* @property {Number} parentOffset
* @property {PmNode} parent
* @property {PmNode} doc
*/
/**
* @typedef {Object} PmSelectionRange
*/
/**
* @typedef {Object} PmTransaction
* @property {Number} time
* @property {PmMark[]|null} storedMarks
* @property {PmSelection} selection
*/
/**
* @typedef {Object} PmFragment
*/
/**
* @typedef {Function} PmCommandHandler
* @param {PmEditorState} state
* @param {PmDispatchFunction} dispatch
*/
/**
* @typedef {Function} PmDispatchFunction
* @param {PmTransaction} tr
*/
/**
* @typedef {Object} PmView
* @param {PmEditorState} state
* @param {Element} dom
* @param {Boolean} editable
* @param {Boolean} composing
*/

View file

@ -1,13 +1,20 @@
import schema from "./schema"; import schema from "./schema";
import {DOMParser, DOMSerializer} from "prosemirror-model"; import {DOMParser, DOMSerializer} from "prosemirror-model";
/**
* @param {String} html
* @return {PmNode}
*/
export function htmlToDoc(html) { export function htmlToDoc(html) {
const renderDoc = document.implementation.createHTMLDocument(); const renderDoc = document.implementation.createHTMLDocument();
renderDoc.body.innerHTML = html; renderDoc.body.innerHTML = html;
return DOMParser.fromSchema(schema).parse(renderDoc.body); return DOMParser.fromSchema(schema).parse(renderDoc.body);
} }
/**
* @param {PmNode} doc
* @return {string}
*/
export function docToHtml(doc) { export function docToHtml(doc) {
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content); const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content);
const renderDoc = document.implementation.createHTMLDocument(); const renderDoc = document.implementation.createHTMLDocument();