From 81dfe9c3453a777f8c1b2ca879e084733f58b5ad Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 7 Jan 2022 21:22:07 +0000 Subject: [PATCH] Got callouts about working, simplified markdown setup --- resources/js/editor/MarkdownView.js | 163 +-------------------- resources/js/editor/markdown-parser.js | 22 +++ resources/js/editor/markdown-serializer.js | 15 ++ resources/js/editor/util.js | 16 ++ 4 files changed, 60 insertions(+), 156 deletions(-) create mode 100644 resources/js/editor/markdown-parser.js create mode 100644 resources/js/editor/markdown-serializer.js create mode 100644 resources/js/editor/util.js diff --git a/resources/js/editor/MarkdownView.js b/resources/js/editor/MarkdownView.js index 30e92713e..f952c8606 100644 --- a/resources/js/editor/MarkdownView.js +++ b/resources/js/editor/MarkdownView.js @@ -1,158 +1,13 @@ -import schema from "./schema"; -import {MarkdownSerializer, MarkdownParser} from "prosemirror-markdown"; -import {DOMParser, DOMSerializer} from "prosemirror-model"; -import markdownit from "markdown-it"; +import {htmlToDoc, docToHtml} from "./util"; - -function listIsTight(tokens, i) { - while (++i < tokens.length) - { if (tokens[i].type != "list_item_open") { return tokens[i].hidden } } - return false -} - -// TODO - Need to tweak parser logic -// so HTML blocks get parsed out using the normal DOMParser logic. -// Likely need to copy & alter the inner parsing logic - -const mdParser = new MarkdownParser(schema, markdownit("commonmark", {html: true}), { - blockquote: {block: "blockquote"}, - paragraph: {block: "paragraph"}, - html_block: { block: "callout", noCloseToken: true, getAttrs: function(tok) { - return { - type: 'info', - } - }}, - list_item: {block: "list_item"}, - bullet_list: {block: "bullet_list", getAttrs: function (_, tokens, i) { return ({tight: listIsTight(tokens, i)}); }}, - ordered_list: {block: "ordered_list", getAttrs: function (tok, tokens, i) { return ({ - order: +tok.attrGet("start") || 1, - tight: listIsTight(tokens, i) - }); }}, - heading: {block: "heading", getAttrs: function (tok) { return ({level: +tok.tag.slice(1)}); }}, - code_block: {block: "code_block", noCloseToken: true}, - fence: {block: "code_block", getAttrs: function (tok) { return ({params: tok.info || ""}); }, noCloseToken: true}, - hr: {node: "horizontal_rule"}, - image: {node: "image", getAttrs: function (tok) { return ({ - src: tok.attrGet("src"), - title: tok.attrGet("title") || null, - alt: tok.children[0] && tok.children[0].content || null - }); }}, - hardbreak: {node: "hard_break"}, - - em: {mark: "em"}, - strong: {mark: "strong"}, - link: {mark: "link", getAttrs: function (tok) { return ({ - href: tok.attrGet("href"), - title: tok.attrGet("title") || null - }); }}, - code_inline: {mark: "code", noCloseToken: true} -}); - -const mdSerializer = new MarkdownSerializer({ - blockquote: function blockquote(state, node) { - state.wrapBlock("> ", null, node, function () { return state.renderContent(node); }); - }, - callout: function(state, node) { - state.write(`

\n`); - state.text(node.textContent, false); - state.ensureNewLine(); - state.write(`

`); - state.closeBlock(node); - }, - code_block: function code_block(state, node) { - state.write("```" + (node.attrs.params || "") + "\n"); - state.text(node.textContent, false); - state.ensureNewLine(); - state.write("```"); - state.closeBlock(node); - }, - heading: function heading(state, node) { - state.write(state.repeat("#", node.attrs.level) + " "); - state.renderInline(node); - state.closeBlock(node); - }, - horizontal_rule: function horizontal_rule(state, node) { - state.write(node.attrs.markup || "---"); - state.closeBlock(node); - }, - bullet_list: function bullet_list(state, node) { - state.renderList(node, " ", function () { return (node.attrs.bullet || "*") + " "; }); - }, - ordered_list: function ordered_list(state, node) { - var start = node.attrs.order || 1; - var maxW = String(start + node.childCount - 1).length; - var space = state.repeat(" ", maxW + 2); - state.renderList(node, space, function (i) { - var nStr = String(start + i); - return state.repeat(" ", maxW - nStr.length) + nStr + ". " - }); - }, - list_item: function list_item(state, node) { - state.renderContent(node); - }, - paragraph: function paragraph(state, node) { - state.renderInline(node); - state.closeBlock(node); - }, - - image: function image(state, node) { - state.write("![" + state.esc(node.attrs.alt || "") + "](" + state.esc(node.attrs.src) + - (node.attrs.title ? " " + state.quote(node.attrs.title) : "") + ")"); - }, - hard_break: function hard_break(state, node, parent, index) { - for (var i = index + 1; i < parent.childCount; i++) - { if (parent.child(i).type != node.type) { - state.write("\\\n"); - return - } } - }, - text: function text(state, node) { - state.text(node.text); - } -}, { - em: {open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true}, - strong: {open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true}, - link: { - open: function open(_state, mark, parent, index) { - return isPlainURL(mark, parent, index, 1) ? "<" : "[" - }, - close: function close(state, mark, parent, index) { - return isPlainURL(mark, parent, index, -1) ? ">" - : "](" + state.esc(mark.attrs.href) + (mark.attrs.title ? " " + state.quote(mark.attrs.title) : "") + ")" - } - }, - code: {open: function open(_state, _mark, parent, index) { return backticksFor(parent.child(index), -1) }, - close: function close(_state, _mark, parent, index) { return backticksFor(parent.child(index - 1), 1) }, - escape: false} -}); - -function backticksFor(node, side) { - var ticks = /`+/g, m, len = 0; - if (node.isText) { while (m = ticks.exec(node.text)) { len = Math.max(len, m[0].length); } } - var result = len > 0 && side > 0 ? " `" : "`"; - for (var i = 0; i < len; i++) { result += "`"; } - if (len > 0 && side < 0) { result += " "; } - return result -} - -function isPlainURL(link, parent, index, side) { - if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) { return false } - var 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 } - var next = parent.child(index + (side < 0 ? -2 : 1)); - return !link.isInSet(next.marks) -} +import parser from "./markdown-parser"; +import serializer from "./markdown-serializer"; class MarkdownView { constructor(target, content) { - // Build DOM from content - const renderDoc = document.implementation.createHTMLDocument(); - renderDoc.body.innerHTML = content; - - const htmlDoc = DOMParser.fromSchema(schema).parse(renderDoc.body); - const markdown = mdSerializer.serialize(htmlDoc); + const htmlDoc = htmlToDoc(content); + const markdown = serializer.serialize(htmlDoc); this.textarea = target.appendChild(document.createElement("textarea")) this.textarea.value = markdown; @@ -160,12 +15,8 @@ class MarkdownView { get content() { const markdown = this.textarea.value; - const doc = mdParser.parse(markdown); - console.log(doc); - const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content); - const renderDoc = document.implementation.createHTMLDocument(); - renderDoc.body.appendChild(fragment); - return renderDoc.body.innerHTML; + const doc = parser.parse(markdown); + return docToHtml(doc); } focus() { this.textarea.focus() } diff --git a/resources/js/editor/markdown-parser.js b/resources/js/editor/markdown-parser.js new file mode 100644 index 000000000..46495a7e0 --- /dev/null +++ b/resources/js/editor/markdown-parser.js @@ -0,0 +1,22 @@ +import schema from "./schema"; +import markdownit from "markdown-it"; +import {MarkdownParser, defaultMarkdownParser} from "prosemirror-markdown"; +import {htmlToDoc} from "./util"; + +const tokens = defaultMarkdownParser.tokens; + +// This is really a placeholder on the object to allow the below +// parser.tokenHandlers.html_block hack to work as desired. +tokens.html_block = {block: "callout", noCloseToken: true}; + +const tokenizer = markdownit("commonmark", {html: true}); +const parser = new MarkdownParser(schema, tokenizer, tokens); + +parser.tokenHandlers.html_block = function(state, tok, tokens, i) { + const contentDoc = htmlToDoc(tok.content || ''); + for (const node of contentDoc.content.content) { + state.addNode(node.type, node.attrs, node.content); + } +}; + +export default parser; \ No newline at end of file diff --git a/resources/js/editor/markdown-serializer.js b/resources/js/editor/markdown-serializer.js new file mode 100644 index 000000000..8e1e2b816 --- /dev/null +++ b/resources/js/editor/markdown-serializer.js @@ -0,0 +1,15 @@ +import {MarkdownSerializer, defaultMarkdownSerializer} from "prosemirror-markdown"; +import {docToHtml} from "./util"; + +const nodes = defaultMarkdownSerializer.nodes; +const marks = defaultMarkdownSerializer.marks; + +nodes.callout = function(state, node) { + const html = docToHtml({ content: [node] }); + state.write(html); + state.closeBlock(); +}; + +const serializer = new MarkdownSerializer(nodes, marks); + +export default serializer; \ No newline at end of file diff --git a/resources/js/editor/util.js b/resources/js/editor/util.js new file mode 100644 index 000000000..ec940bd2b --- /dev/null +++ b/resources/js/editor/util.js @@ -0,0 +1,16 @@ +import schema from "./schema"; +import {DOMParser, DOMSerializer} from "prosemirror-model"; + + +export function htmlToDoc(html) { + const renderDoc = document.implementation.createHTMLDocument(); + renderDoc.body.innerHTML = html; + return DOMParser.fromSchema(schema).parse(renderDoc.body); +} + +export function docToHtml(doc) { + const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content); + const renderDoc = document.implementation.createHTMLDocument(); + renderDoc.body.appendChild(fragment); + return renderDoc.body.innerHTML; +} \ No newline at end of file