diff --git a/TODO b/TODO index ca678d1dc..ce7cdbf07 100644 --- a/TODO +++ b/TODO @@ -29,4 +29,10 @@ - If no marks, clear the block type if text type? - Remove links button? (Action already in place if link href is empty). - Links - Limit target attribute options and validate URL. -- Links - Integrate entity picker. \ No newline at end of file +- 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. \ No newline at end of file diff --git a/resources/js/editor/commands.js b/resources/js/editor/commands.js index 1817bd2a9..bd71ceba3 100644 --- a/resources/js/editor/commands.js +++ b/resources/js/editor/commands.js @@ -1,3 +1,8 @@ +/** + * @param {String} attrName + * @param {String} attrValue + * @return {PmCommandHandler} + */ export function setBlockAttr(attrName, attrValue) { return function (state, dispatch) { const ref = state.selection; @@ -37,6 +42,10 @@ export function setBlockAttr(attrName, attrValue) { } } +/** + * @param {PmNodeType} blockType + * @return {PmCommandHandler} + */ export function insertBlockBefore(blockType) { return function (state, dispatch) { const startPosition = state.selection.$from.before(1); @@ -49,6 +58,9 @@ export function insertBlockBefore(blockType) { } } +/** + * @return {PmCommandHandler} + */ export function removeMarks() { return function (state, dispatch) { if (dispatch) { diff --git a/resources/js/editor/markdown-parser.js b/resources/js/editor/markdown-parser.js index b198ffad4..d60ddd554 100644 --- a/resources/js/editor/markdown-parser.js +++ b/resources/js/editor/markdown-parser.js @@ -48,6 +48,10 @@ parser.tokenHandlers.html_inline = function(state, tok, tokens, i) { } } +/** + * @param {String} html + * @return {PmMark[]} + */ function extractMarksFromHtml(html) { const contentDoc = htmlToDoc('
' + (html || '') + '
'); const marks = contentDoc?.content?.content?.[0]?.content?.content?.[0]?.marks; diff --git a/resources/js/editor/markdown-serializer.js b/resources/js/editor/markdown-serializer.js index 5e1dfb33b..2edc1ef27 100644 --- a/resources/js/editor/markdown-serializer.js +++ b/resources/js/editor/markdown-serializer.js @@ -1,14 +1,45 @@ -import {MarkdownSerializer, defaultMarkdownSerializer} from "prosemirror-markdown"; +import {MarkdownSerializer, defaultMarkdownSerializer, MarkdownSerializerState} from "prosemirror-markdown"; import {docToHtml} from "./util"; const nodes = defaultMarkdownSerializer.nodes; const marks = defaultMarkdownSerializer.marks; -nodes.callout = function(state, node) { +nodes.callout = function (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 `` + } + return isPlainURL(mark, parent, index, 1) ? "<" : "[" + }, + close(state, mark, parent, index) { + if (mark.attrs.target) { + return ``; + } + return isPlainURL(mark, parent, index, -1) ? ">" + : "](" + state.esc(mark.attrs.href) + (mark.attrs.title ? " " + state.quote(mark.attrs.title) : "") + ")" + } +}; marks.underline = { open: '', @@ -44,9 +75,12 @@ marks.background_color = { close: '', }; - +/** + * @param {MarkdownSerializerState} state + * @param node + */ function writeNodeAsHtml(state, node) { - const html = docToHtml({ content: [node] }); + const html = docToHtml({content: [node]}); state.write(html); state.ensureNewLine(); state.write('\n'); @@ -57,7 +91,7 @@ function writeNodeAsHtml(state, node) { // or element that cannot be represented in commonmark without losing // formatting or content. 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) { writeNodeAsHtml(state, node); } else { diff --git a/resources/js/editor/menu/item-anchor-button.js b/resources/js/editor/menu/item-anchor-button.js index f41931427..d95ac2e78 100644 --- a/resources/js/editor/menu/item-anchor-button.js +++ b/resources/js/editor/menu/item-anchor-button.js @@ -6,7 +6,11 @@ import schema from "../schema"; import {MenuItem} from "./menu"; import {icons} from "./icons"; - +/** + * @param {PmMarkType} markType + * @param {String} attribute + * @return {(function(PmEditorState): (string|null))} + */ function getMarkAttribute(markType, attribute) { return function (state) { 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) { return new DialogBox([ new DialogForm([ @@ -64,6 +73,12 @@ function applyLink(formData, state, dispatch) { return true; } +/** + * @param {PmEditorState} state + * @param {PmDispatchFunction} dispatch + * @param {PmView} view + * @param {Event} e + */ function onPress(state, dispatch, view, e) { const dialog = getLinkDialog((data) => { applyLink(data, state, dispatch); @@ -77,6 +92,9 @@ function onPress(state, dispatch, view, e) { document.body.appendChild(dom); } +/** + * @return {MenuItem} + */ function anchorButtonItem() { return new MenuItem({ title: "Insert/Edit Anchor Link", diff --git a/resources/js/editor/notes.md b/resources/js/editor/notes.md deleted file mode 100644 index c4588551a..000000000 --- a/resources/js/editor/notes.md +++ /dev/null @@ -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. -- \ No newline at end of file diff --git a/resources/js/editor/schema.js b/resources/js/editor/schema.js index 6aa230b21..e0cd25222 100644 --- a/resources/js/editor/schema.js +++ b/resources/js/editor/schema.js @@ -3,9 +3,10 @@ import {Schema} from "prosemirror-model"; import nodes from "./schema-nodes"; import marks from "./schema-marks"; -const index = new Schema({ +/** @var {PmSchema} schema */ +const schema = new Schema({ nodes, marks, }); -export default index; \ No newline at end of file +export default schema; \ No newline at end of file diff --git a/resources/js/editor/types.js b/resources/js/editor/types.js new file mode 100644 index 000000000..b676eb107 --- /dev/null +++ b/resources/js/editor/types.js @@ -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