@@ -4,54 +4,54 @@ import markdownit from "markdown-it"; | |||||
import { writeFreelySchema } from "./schema"; | import { writeFreelySchema } from "./schema"; | ||||
export const writeAsMarkdownParser = new MarkdownParser( | export const writeAsMarkdownParser = new MarkdownParser( | ||||
writeFreelySchema, | |||||
markdownit("commonmark", { html: true }), | |||||
{ | |||||
// 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", noCloseToken: true }, | |||||
fence: { | |||||
block: "code_block", | |||||
getAttrs: (tok) => ({ params: tok.info || "" }), | |||||
noCloseToken: true, | |||||
}, | |||||
// hr: { node: "horizontal_rule" }, | |||||
image: { | |||||
node: "image", | |||||
getAttrs: (tok) => ({ | |||||
src: tok.attrGet("src"), | |||||
title: tok.attrGet("title") || null, | |||||
alt: tok.children?.[0].content || null, | |||||
}), | |||||
}, | |||||
hardbreak: { node: "hard_break" }, | |||||
writeFreelySchema, | |||||
markdownit("commonmark", { html: true }), | |||||
{ | |||||
// 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", noCloseToken: true }, | |||||
fence: { | |||||
block: "code_block", | |||||
getAttrs: (tok) => ({ params: tok.info || "" }), | |||||
noCloseToken: true, | |||||
}, | |||||
// hr: { node: "horizontal_rule" }, | |||||
image: { | |||||
node: "image", | |||||
getAttrs: (tok) => ({ | |||||
src: tok.attrGet("src"), | |||||
title: tok.attrGet("title") || null, | |||||
alt: tok.children?.[0].content || null, | |||||
}), | |||||
}, | |||||
hardbreak: { node: "hard_break" }, | |||||
em: { mark: "em" }, | |||||
strong: { mark: "strong" }, | |||||
link: { | |||||
mark: "link", | |||||
getAttrs: (tok) => ({ | |||||
href: tok.attrGet("href"), | |||||
title: tok.attrGet("title") || null, | |||||
}), | |||||
}, | |||||
code_inline: { mark: "code", noCloseToken: true }, | |||||
html_block: { | |||||
node: "readmore", | |||||
getAttrs(token) { | |||||
// TODO: Give different attributes depending on the token content | |||||
return {}; | |||||
}, | |||||
}, | |||||
em: { mark: "em" }, | |||||
strong: { mark: "strong" }, | |||||
link: { | |||||
mark: "link", | |||||
getAttrs: (tok) => ({ | |||||
href: tok.attrGet("href"), | |||||
title: tok.attrGet("title") || null, | |||||
}), | |||||
}, | |||||
code_inline: { mark: "code", noCloseToken: true }, | |||||
html_block: { | |||||
node: "readmore", | |||||
getAttrs(token) { | |||||
// TODO: Give different attributes depending on the token content | |||||
return {}; | |||||
}, | |||||
}, | }, | ||||
} | |||||
); | ); |
@@ -47,10 +47,6 @@ export const writeAsMarkdownSerializer = new MarkdownSerializer( | |||||
state.renderInline(node); | state.renderInline(node); | ||||
state.closeBlock(node); | state.closeBlock(node); | ||||
}, | }, | ||||
// horizontal_rule(state, node) { | |||||
// state.write(node.attrs.markup || "---"); | |||||
// state.closeBlock(node); | |||||
// }, | |||||
bullet_list(state, node) { | bullet_list(state, node) { | ||||
state.renderList(node, " ", () => `${node.attrs.bullet || "*"} `); | state.renderList(node, " ", () => `${node.attrs.bullet || "*"} `); | ||||
}, | }, | ||||
@@ -75,13 +71,13 @@ export const writeAsMarkdownSerializer = new MarkdownSerializer( | |||||
state.write( | state.write( | ||||
`![${state.esc(node.attrs.alt || "")}](${state.esc(node.attrs.src)}${ | `![${state.esc(node.attrs.alt || "")}](${state.esc(node.attrs.src)}${ | ||||
node.attrs.title ? ` ${state.quote(node.attrs.title)}` : "" | node.attrs.title ? ` ${state.quote(node.attrs.title)}` : "" | ||||
})`, | |||||
})` | |||||
); | ); | ||||
}, | }, | ||||
hard_break(state, node, parent, index) { | hard_break(state, node, parent, index) { | ||||
for (let i = index + 1; i < parent.childCount; i += 1) | for (let i = index + 1; i < parent.childCount; i += 1) | ||||
if (parent.child(i).type !== node.type) { | if (parent.child(i).type !== node.type) { | ||||
state.write("\n"); | |||||
state.write("\\\n"); | |||||
return; | return; | ||||
} | } | ||||
}, | }, | ||||
@@ -123,5 +119,5 @@ export const writeAsMarkdownSerializer = new MarkdownSerializer( | |||||
}, | }, | ||||
escape: false, | escape: false, | ||||
}, | }, | ||||
}, | |||||
} | |||||
); | ); |
@@ -4,23 +4,29 @@ import { buildMenuItems } from "prosemirror-example-setup"; | |||||
import { writeFreelySchema } from "./schema"; | import { writeFreelySchema } from "./schema"; | ||||
function canInsert(state, nodeType, attrs) { | function canInsert(state, nodeType, attrs) { | ||||
let $from = state.selection.$from | |||||
for (let d = $from.depth; d >= 0; d--) { | |||||
let index = $from.index(d) | |||||
if ($from.node(d).canReplaceWith(index, index, nodeType, attrs)) return true | |||||
} | |||||
return false | |||||
let $from = state.selection.$from; | |||||
for (let d = $from.depth; d >= 0; d--) { | |||||
let index = $from.index(d); | |||||
if ($from.node(d).canReplaceWith(index, index, nodeType, attrs)) | |||||
return true; | |||||
} | } | ||||
return false; | |||||
} | |||||
const ReadMoreItem = new MenuItem({ | const ReadMoreItem = new MenuItem({ | ||||
label: "Read more", | |||||
select: (state) => canInsert(state, writeFreelySchema.nodes.readmore), | |||||
run(state, dispatch) { | |||||
dispatch(state.tr.replaceSelectionWith(writeFreelySchema.nodes.readmore.create())) | |||||
}, | |||||
label: "Read more", | |||||
select: (state) => canInsert(state, writeFreelySchema.nodes.readmore), | |||||
run(state, dispatch) { | |||||
dispatch( | |||||
state.tr.replaceSelectionWith(writeFreelySchema.nodes.readmore.create()) | |||||
); | |||||
}, | |||||
}); | }); | ||||
export const getMenu = ()=> { | |||||
const menuContent = [...buildMenuItems(writeFreelySchema).fullMenu, [ReadMoreItem]]; | |||||
return menuContent | |||||
} | |||||
export const getMenu = () => { | |||||
const menuContent = [ | |||||
...buildMenuItems(writeFreelySchema).fullMenu, | |||||
[ReadMoreItem], | |||||
]; | |||||
return menuContent; | |||||
}; |
@@ -6,5 +6,9 @@ | |||||
<!-- <label> <input type=radio name=inputformat value=prosemirror checked> WYSIWYM</label> --> | <!-- <label> <input type=radio name=inputformat value=prosemirror checked> WYSIWYM</label> --> | ||||
<!-- </div> --> | <!-- </div> --> | ||||
<div style="display: none"><textarea id="content">This is a comment written in [Markdown](http://commonmark.org). *You* may know the syntax for inserting a link, but does your whole audience? So you can give people the **choice** to use a more familiar, discoverable interface.</textarea></div> | |||||
<div style="display: none"> | |||||
<textarea id="content"> | |||||
This is a comment written in [Markdown](http://commonmark.org). *You* may know the syntax for inserting a link, but does your whole audience? So you can give people the **choice** to use a more familiar, discoverable interface.</textarea | |||||
> | |||||
</div> | |||||
<script src="dist/prose.bundle.js"></script> | <script src="dist/prose.bundle.js"></script> |
@@ -9,89 +9,101 @@ | |||||
// destroy() { this.textarea.remove() } | // destroy() { this.textarea.remove() } | ||||
// } | // } | ||||
import { EditorView } from "prosemirror-view" | |||||
import { EditorState } from "prosemirror-state" | |||||
import { exampleSetup } from "prosemirror-example-setup" | |||||
import { EditorView } from "prosemirror-view"; | |||||
import { EditorState, TextSelection } from "prosemirror-state"; | |||||
import { exampleSetup } from "prosemirror-example-setup"; | |||||
import { keymap } from "prosemirror-keymap"; | import { keymap } from "prosemirror-keymap"; | ||||
import { writeAsMarkdownParser } from "./markdownParser" | |||||
import { writeAsMarkdownSerializer } from "./markdownSerializer" | |||||
import { writeFreelySchema } from "./schema" | |||||
import { getMenu } from "./menu" | |||||
import { writeAsMarkdownParser } from "./markdownParser"; | |||||
import { writeAsMarkdownSerializer } from "./markdownSerializer"; | |||||
import { writeFreelySchema } from "./schema"; | |||||
import { getMenu } from "./menu"; | |||||
let $title = document.querySelector('#title') | |||||
let $content = document.querySelector('#content') | |||||
let $title = document.querySelector("#title"); | |||||
let $content = document.querySelector("#content"); | |||||
class ProseMirrorView { | |||||
constructor(target, content) { | |||||
this.view = new EditorView(target, { | |||||
state: EditorState.create({ | |||||
doc: function (content) { | |||||
// console.log('loading '+window.draftKey) | |||||
let localDraft = localStorage.getItem(window.draftKey); | |||||
if (localDraft != null) { | |||||
content = localDraft | |||||
} | |||||
if (content.indexOf("# ") === 0) { | |||||
let eol = content.indexOf("\n"); | |||||
let title = content.substring("# ".length, eol); | |||||
content = content.substring(eol + "\n\n".length); | |||||
$title.value = title; | |||||
} | |||||
return writeAsMarkdownParser.parse(content) | |||||
}(content), | |||||
plugins: [ | |||||
keymap({ | |||||
"Mod-Enter": () => { | |||||
document.getElementById("publish").click(); | |||||
return true; | |||||
}, | |||||
"Mod-k": ()=> { | |||||
console.log("TODO-link"); | |||||
return true; | |||||
} | |||||
}), | |||||
...exampleSetup({ schema: writeFreelySchema, menuContent: getMenu() }), | |||||
// Bugs: | |||||
// 1. When there's just an empty line and a hard break is inserted with shift-enter then two enters are inserted | |||||
// which do not show up in the markdown ( maybe bc. they are training enters ) | |||||
] | |||||
}), | |||||
dispatchTransaction(transaction) { | |||||
// console.log('saving to '+window.draftKey) | |||||
const newContent = writeAsMarkdownSerializer.serialize(transaction.doc) | |||||
console.log({newContent}) | |||||
$content.value = newContent | |||||
localStorage.setItem(window.draftKey, function () { | |||||
let draft = ""; | |||||
if ($title.value != null && $title.value !== "") { | |||||
draft = "# " + $title.value + "\n\n" | |||||
} | |||||
draft += $content.value | |||||
return draft | |||||
}()); | |||||
let newState = this.state.apply(transaction) | |||||
this.updateState(newState) | |||||
} | |||||
}) | |||||
class ProseMirrorView { | |||||
constructor(target, content) { | |||||
let localDraft = localStorage.getItem(window.draftKey); | |||||
if (localDraft != null) { | |||||
content = localDraft; | |||||
} | } | ||||
get content() { | |||||
return defaultMarkdownSerializer.serialize(this.view.state.doc) | |||||
if (content.indexOf("# ") === 0) { | |||||
let eol = content.indexOf("\n"); | |||||
let title = content.substring("# ".length, eol); | |||||
content = content.substring(eol + "\n\n".length); | |||||
$title.value = title; | |||||
} | } | ||||
focus() { this.view.focus() } | |||||
destroy() { this.view.destroy() } | |||||
} | |||||
let place = document.querySelector("#editor") | |||||
let view = new ProseMirrorView(place, $content.value) | |||||
const doc = writeAsMarkdownParser.parse( | |||||
// Replace all "solo" \n's with \\\n for correct markdown parsing | |||||
content.replaceAll(/(?<!\n)\n(?!\n)/g, "\\\n") | |||||
); | |||||
this.view = new EditorView(target, { | |||||
state: EditorState.create({ | |||||
doc, | |||||
plugins: [ | |||||
keymap({ | |||||
"Mod-Enter": () => { | |||||
document.getElementById("publish").click(); | |||||
return true; | |||||
}, | |||||
"Mod-k": () => { | |||||
const linkButton = document.querySelector(".ProseMirror-icon[title='Add or remove link']") | |||||
linkButton.dispatchEvent(new Event('mousedown')); | |||||
return true; | |||||
}, | |||||
}), | |||||
...exampleSetup({ | |||||
schema: writeFreelySchema, | |||||
menuContent: getMenu(), | |||||
}), | |||||
], | |||||
}), | |||||
dispatchTransaction(transaction) { | |||||
const newContent = writeAsMarkdownSerializer | |||||
.serialize(transaction.doc) | |||||
// Replace all \\\ns ( not followed by a \n ) with \n | |||||
.replaceAll(/\\\n(?!\n)/g, "\n"); | |||||
$content.value = newContent; | |||||
let draft = ""; | |||||
if ($title.value != null && $title.value !== "") { | |||||
draft = "# " + $title.value + "\n\n"; | |||||
} | |||||
draft += newContent; | |||||
localStorage.setItem(window.draftKey, draft); | |||||
let newState = this.state.apply(transaction); | |||||
this.updateState(newState); | |||||
}, | |||||
}); | |||||
// Editor is focused to the last position. This is a workaround for a bug: | |||||
// 1. 1 type something in an existing entry | |||||
// 2. reload - works fine, the draft is reloaded | |||||
// 3. reload again - the draft is somehow removed from localStorage and the original content is loaded | |||||
// When the editor is focused the content is re-saved to localStorage | |||||
// This is also useful for editing, so it's not a bad thing even | |||||
const lastPosition = this.view.state.doc.content.size; | |||||
const selection = TextSelection.create(this.view.state.doc, lastPosition); | |||||
this.view.dispatch(this.view.state.tr.setSelection(selection)); | |||||
this.view.focus(); | |||||
} | |||||
get content() { | |||||
return defaultMarkdownSerializer.serialize(this.view.state.doc); | |||||
} | |||||
focus() { | |||||
this.view.focus(); | |||||
} | |||||
destroy() { | |||||
this.view.destroy(); | |||||
} | |||||
} | |||||
// document.querySelectorAll("input[type=radio]").forEach(button => { | |||||
// button.addEventListener("change", () => { | |||||
// if (!button.checked) return | |||||
// let View = button.value == "markdown" ? MarkdownView : ProseMirrorView | |||||
// if (view instanceof View) return | |||||
// let content = view.content | |||||
// view.destroy() | |||||
// view = new View(place, content) | |||||
// view.focus() | |||||
// }) | |||||
// }) | |||||
let place = document.querySelector("#editor"); | |||||
let view = new ProseMirrorView(place, $content.value); |
@@ -1,16 +1,21 @@ | |||||
import { schema } from "prosemirror-markdown" | |||||
import { schema } from "prosemirror-markdown"; | |||||
import { Schema } from "prosemirror-model"; | import { Schema } from "prosemirror-model"; | ||||
export const writeFreelySchema = new Schema({ | export const writeFreelySchema = new Schema({ | ||||
nodes: schema.spec.nodes.remove("blockquote") | |||||
.remove("horizontal_rule") | |||||
.addToEnd("readmore", { | |||||
inline: false, | |||||
content: "", | |||||
group: "block", | |||||
draggable: true, | |||||
toDOM: (node) => ["div", { class: "editorreadmore", style: "width: 100%;text-align:center" }, "Read more..."], | |||||
parseDOM: [{ tag: "div.editorreadmore" }], | |||||
}), | |||||
marks: schema.spec.marks, | |||||
nodes: schema.spec.nodes | |||||
.remove("blockquote") | |||||
.remove("horizontal_rule") | |||||
.addToEnd("readmore", { | |||||
inline: false, | |||||
content: "", | |||||
group: "block", | |||||
draggable: true, | |||||
toDOM: (node) => [ | |||||
"div", | |||||
{ class: "editorreadmore", style: "width: 100%;text-align:center" }, | |||||
"Read more...", | |||||
], | |||||
parseDOM: [{ tag: "div.editorreadmore" }], | |||||
}), | |||||
marks: schema.spec.marks, | |||||
}); | }); |