diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index f557aa36..b4fcc3a7 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -30,9 +30,9 @@ import "prosemirror-gapcursor/style/gapcursor.css" import {inputRules, wrappingInputRule, textblockTypeInputRule, emDash, ellipsis} from "prosemirror-inputrules" -// markdown (commonmark de markdown-it) -import {schema, defaultMarkdownParser, - defaultMarkdownSerializer} from "prosemirror-markdown" +import serializer from './serializer.js' +import parser from './deserializer.js' +import schema from './schema.js' import {toggleMark, setBlockType, wrapIn, baseKeymap} from "prosemirror-commands" import {toggleWrap, toggleBlockType, toggleList, @@ -42,6 +42,7 @@ import {startImageUpload, placeholderPlugin} from "./imageUpload" const toggleBold = toggleMark(schema.marks.strong) const toggleItalic = toggleMark(schema.marks.em) +const toggleHighlight = toggleMark(schema.marks.mark) const wrapInQuote = toggleWrap(schema.nodes.blockquote) const toggleUl = toggleList(schema.nodes.bullet_list, schema.nodes.list_item) @@ -96,6 +97,7 @@ class SuttyEditor { } setupBtn('bold', () => this.runCommand(toggleBold)) setupBtn('italic', () => this.runCommand(toggleItalic)) + setupBtn('highlight', () => this.runCommand(toggleHighlight)) setupBtn('quote', () => this.runCommand(wrapInQuote)) setupBtn('link', () => this.runCommand(makeLink)) setupBtn('ul', () => this.runCommand(toggleUl)) @@ -121,7 +123,7 @@ class SuttyEditor { return EditorState.create({ schema, ...(markdown ? { - doc: defaultMarkdownParser.parse(markdown) + doc: parser.parse(markdown) } : {}), plugins: [ gapCursor(), @@ -142,39 +144,49 @@ class SuttyEditor { return new EditorView(element, {state}) } - get markdown () { - return defaultMarkdownSerializer.serialize(this.view.state.doc) + getMarkdown () { + return serializer.serialize(this.view.state.doc) } } document.addEventListener('turbolinks:load', e => { - const editorEls = document.querySelectorAll('.sutty-editor-prosemirror') - for (const el of editorEls) { - // conseguir textarea con el markdown - const textareaEl = document.querySelector( - `.sutty-editor-content[data-sutty-identifier="${el.dataset.suttyIdentifier}"]` - ) + try { + const editorEls = document.querySelectorAll('.sutty-editor-prosemirror') + for (const el of editorEls) { + // conseguir textarea con el markdown + const textareaEl = document.querySelector( + `.sutty-editor-content[data-sutty-identifier="${el.dataset.suttyIdentifier}"]` + ) - // conseguir toolbar - const toolbarEl = document.querySelector( - `.sutty-editor-toolbar[data-sutty-identifier="${el.dataset.suttyIdentifier}"]` - ) + // conseguir toolbar + const toolbarEl = document.querySelector( + `.sutty-editor-toolbar[data-sutty-identifier="${el.dataset.suttyIdentifier}"]` + ) - // ocultarlo (no se oculta originalmente para usuaries noscript - textareaEl.style.display = 'none' - - // crear editor con el markdown del textarea - const editor = new SuttyEditor({ - element: el, - markdown: textareaEl.value, - toolbar: toolbarEl, - }) + // ocultarlo (no se oculta originalmente para usuaries noscript) + textareaEl.style.display = 'none' + + // crear editor con el markdown del textarea + const editor = new SuttyEditor({ + element: el, + markdown: textareaEl.value, + toolbar: toolbarEl, + }) - // hookear a la form para inyectar el contenido del editor al textarea oculto para que se mande - textareaEl.form.addEventListener('submit', e => { - console.log('enviando formulario: inyectando markdown en textarea...') - textareaEl.value = editor.markdown - }, true) + // hookear a la form para inyectar el contenido del editor al textarea oculto para que se mande + textareaEl.form.addEventListener('submit', e => { + console.log('enviando formulario: inyectando markdown en textarea...') + try { + textareaEl.value = editor.getMarkdown() + } catch (error) { + console.error('no se pudo inyectar markdown!', error) + // TODO: mostrar esto bien + e.preventDefault() + } + }, true) + } + } catch (error) { + console.error('algo falló en la carga del editor', error) } const table = document.querySelector('.table-draggable') diff --git a/app/javascript/packs/deserializer.js b/app/javascript/packs/deserializer.js new file mode 100644 index 00000000..73772ccb --- /dev/null +++ b/app/javascript/packs/deserializer.js @@ -0,0 +1,37 @@ +// From https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js +import {MarkdownParser} from 'prosemirror-markdown' +import markdownit from "markdown-it" +import markdownitMark from "markdown-it-mark" + +import schema from './schema.js' + +export default new MarkdownParser( + schema, + markdownit("commonmark", {html: false}).use(markdownitMark), + { + blockquote: {block: "blockquote"}, + paragraph: {block: "paragraph"}, + list_item: {block: "list_item"}, + bullet_list: {block: "bullet_list"}, + ordered_list: {block: "ordered_list", getAttrs: tok => ({order: +tok.attrGet("start") || 1})}, + heading: {block: "heading", getAttrs: tok => ({level: +tok.tag.slice(1)})}, + code_block: {block: "code_block"}, + fence: {block: "code_block", getAttrs: tok => ({params: tok.info || ""})}, + hr: {node: "horizontal_rule"}, + image: {node: "image", getAttrs: tok => ({ + 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"}, + mark: {mark: "mark"}, + link: {mark: "link", getAttrs: tok => ({ + href: tok.attrGet("href"), + title: tok.attrGet("title") || null + })}, + code_inline: {mark: "code"}, + }, +) diff --git a/app/javascript/packs/schema.js b/app/javascript/packs/schema.js new file mode 100644 index 00000000..77fc142d --- /dev/null +++ b/app/javascript/packs/schema.js @@ -0,0 +1,11 @@ +import {schema as defaultMarkdownSchema, defaultMarkdownParser} from "prosemirror-markdown" +import {Schema} from "prosemirror-model" + +export default new Schema({ + nodes: defaultMarkdownSchema.spec.nodes, + marks: defaultMarkdownSchema.spec.marks.addBefore('text', 'mark', { + toDOM: node => ['mark'], + parseDOM: [{ tag: 'mark' }], + }), +}) + diff --git a/app/javascript/packs/serializer.js b/app/javascript/packs/serializer.js new file mode 100644 index 00000000..67570f3d --- /dev/null +++ b/app/javascript/packs/serializer.js @@ -0,0 +1,74 @@ +// From https://raw.githubusercontent.com/ProseMirror/prosemirror-markdown/master/src/to_markdown.js +import {MarkdownSerializer} from "prosemirror-markdown" + +export default new MarkdownSerializer({ + blockquote(state, node) { + state.wrapBlock("> ", null, node, () => state.renderContent(node)) + }, + code_block(state, node) { + state.write("```" + (node.attrs.params || "") + "\n") + state.text(node.textContent, false) + state.ensureNewLine() + state.write("```") + state.closeBlock(node) + }, + heading(state, node) { + state.write(state.repeat("#", node.attrs.level) + " ") + state.renderInline(node) + state.closeBlock(node) + }, + horizontal_rule(state, node) { + state.write(node.attrs.markup || "---") + state.closeBlock(node) + }, + bullet_list(state, node) { + state.renderList(node, " ", () => (node.attrs.bullet || "*") + " ") + }, + ordered_list(state, node) { + let start = node.attrs.order || 1 + let maxW = String(start + node.childCount - 1).length + let space = state.repeat(" ", maxW + 2) + state.renderList(node, space, i => { + let nStr = String(start + i) + return state.repeat(" ", maxW - nStr.length) + nStr + ". " + }) + }, + list_item(state, node) { + state.renderContent(node) + }, + paragraph(state, node) { + state.renderInline(node) + state.closeBlock(node) + }, + + 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(state, node, parent, index) { + for (let i = index + 1; i < parent.childCount; i++) + if (parent.child(i).type != node.type) { + state.write("\\\n") + return + } + }, + text(state, node) { + state.text(node.text) + } +}, { + em: {open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true}, + strong: {open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true}, + mark: {open: "==", close: "==", mixable: true, expelEnclosingWhitespace: true}, + link: { + open(_state, mark, parent, index) { + return isPlainURL(mark, parent, index, 1) ? "<" : "[" + }, + 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(_state, _mark, parent, index) { return backticksFor(parent.child(index), -1) }, + close(_state, _mark, parent, index) { return backticksFor(parent.child(index - 1), 1) }, + escape: false} +}) diff --git a/app/views/application/_editor.haml b/app/views/application/_editor.haml index 75922efd..9b35f4d4 100644 --- a/app/views/application/_editor.haml +++ b/app/views/application/_editor.haml @@ -2,6 +2,7 @@ .sutty-editor-toolbar{ 'data-sutty-identifier': identifier } %button.bold-btn= "bold" %button.italic-btn= "italic" + %button.highlight-btn= "highlight" %button.quote-btn= "quote" %button.ul-btn= "ul" %button.ol-btn= "ol" diff --git a/package.json b/package.json index 3106dad1..c7d6025b 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "commonmark": "^0.29.0", "input-map": "https://0xacab.org/sutty/input-map.git", "input-tag": "https://0xacab.org/sutty/input-tag.git", + "markdown-it": "^10.0.0", + "markdown-it-mark": "^3.0.0", "prosemirror-commands": "^1.0.8", "prosemirror-gapcursor": "^1.0.4", "prosemirror-inputrules": "^1.0.4", diff --git a/yarn.lock b/yarn.lock index b0967187..621cb2dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2478,7 +2478,7 @@ enhanced-resolve@4.1.0, enhanced-resolve@^4.1.0: memory-fs "^0.4.0" tapable "^1.0.0" -entities@^2.0.0: +entities@^2.0.0, entities@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== @@ -4035,6 +4035,22 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markdown-it-mark@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/markdown-it-mark/-/markdown-it-mark-3.0.0.tgz#27c3e39ef3cc310b2dde5375082c9fa912983cda" + integrity sha512-HqMWeKfMMOu4zBO0emmxsoMWmbf2cPKZY1wP6FsTbKmicFfp5y4L3KXAsNeO1rM6NTJVOrNlLKMPjWzriBGspw== + +markdown-it@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc" + integrity sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg== + dependencies: + argparse "^1.0.7" + entities "~2.0.0" + linkify-it "^2.0.0" + mdurl "^1.0.1" + uc.micro "^1.0.5" + markdown-it@^8.4.2: version "8.4.2" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54"