"build:js:dev": "esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main",
"build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
"build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main --minify",
- "build:js:editor:dev": "esbuild --bundle ./resources/js/editor.js --outfile=public/dist/editor.js --sourcemap --target=es2019 --main-fields=module,main",
- "build:js:editor:watch": "chokidar --initial \"./resources/js/editor.js\" \"./resources/js/editor/**/*.js\" -c \"npm run build:js:editor:dev\"",
+ "build:js_editor:dev": "esbuild --bundle ./resources/js/editor.js --outfile=public/dist/editor.js --sourcemap --target=es2019 --main-fields=module,main",
+ "build:js_editor:watch": "chokidar --initial \"./resources/js/editor.js\" \"./resources/js/editor/**/*.js\" -c \"npm run build:js_editor:dev\"",
"build": "npm-run-all --parallel build:*:dev",
"production": "npm-run-all --parallel build:*:production",
"dev": "npm-run-all --parallel watch livereload",
this.textarea = target.appendChild(document.createElement("textarea"))
this.textarea.value = markdown;
+ this.textarea.style.width = '1000px';
+ this.textarea.style.height = '1000px';
}
get content() {
import schema from "./schema";
import markdownit from "markdown-it";
import {MarkdownParser, defaultMarkdownParser} from "prosemirror-markdown";
-import {htmlToDoc} from "./util";
+import {htmlToDoc, KeyedMultiStack} 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.
+// These are really a placeholder on the object to allow the below
+// parser.tokenHandlers.html_[block/inline] hacks to work as desired.
tokens.html_block = {block: "callout", noCloseToken: true};
+tokens.html_inline = {mark: "underline"};
const tokenizer = markdownit("commonmark", {html: true});
const parser = new MarkdownParser(schema, tokenizer, tokens);
+// When we come across HTML blocks we use the document schema to parse them
+// into nodes then re-add those back into the parser state.
parser.tokenHandlers.html_block = function(state, tok, tokens, i) {
const contentDoc = htmlToDoc(tok.content || '');
for (const node of contentDoc.content.content) {
}
};
+// When we come across inline HTML we parse out the tag and keep track of
+// that in a stack, along with the marks they parse out to.
+// We open/close the marks within the state depending on the tag open/close type.
+const tagStack = new KeyedMultiStack();
+parser.tokenHandlers.html_inline = function(state, tok, tokens, i) {
+ const isClosing = tok.content.startsWith('</');
+ const isSelfClosing = tok.content.endsWith('/>');
+ const tagName = parseTagNameFromHtmlTokenContent(tok.content);
+
+ if (!isClosing) {
+ const completeTag = isSelfClosing ? tok.content : `${tok.content}a</${tagName}>`;
+ const marks = extractMarksFromHtml(completeTag);
+ tagStack.push(tagName, marks);
+ for (const mark of marks) {
+ state.openMark(mark);
+ }
+ }
+
+ if (isSelfClosing || isClosing) {
+ const marks = (tagStack.pop(tagName) || []).reverse();
+ for (const mark of marks) {
+ state.closeMark(mark);
+ }
+ }
+}
+
+function extractMarksFromHtml(html) {
+ const contentDoc = htmlToDoc('<p>' + (html || '') + '</p>');
+ const marks = contentDoc?.content?.content?.[0]?.content?.content?.[0]?.marks;
+ return marks || [];
+}
+
+/**
+ * @param {string} tokenContent
+ * @return {string}
+ */
+function parseTagNameFromHtmlTokenContent(tokenContent) {
+ return tokenContent.split(' ')[0].replace(/[<>\/]/g, '').toLowerCase();
+}
+
export default parser;
\ No newline at end of file
const marks = defaultMarkdownSerializer.marks;
nodes.callout = function(state, node) {
+ writeNodeAsHtml(state, node);
+};
+
+marks.underline = {
+ open: '<span style="text-decoration: underline;">',
+ close: '</span>',
+};
+
+function writeNodeAsHtml(state, node) {
const html = docToHtml({ content: [node] });
state.write(html);
state.closeBlock();
-};
+}
+
const serializer = new MarkdownSerializer(nodes, marks);
-/**
- * Much of this code originates from https://github.com/ProseMirror/prosemirror-menu
- * and is hence subject to the MIT license found here:
- * https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
- * @copyright Marijn Haverbeke and others
- */
-
import {
MenuItem, Dropdown, DropdownSubmenu, renderGrouped, icons, joinUpItem, liftItem, selectParentNodeItem,
undoItem, redoItem, wrapItem, blockTypeItem
const inlineStyles = [
markItem(schema.marks.strong, {title: "Bold", icon: icons.strong}),
markItem(schema.marks.em, {title: "Italic", icon: icons.em}),
+ markItem(schema.marks.underline, {title: "Underline", label: 'U'}),
];
const formats = [
floating: false,
content: [
[undoItem, redoItem],
- inlineStyles,
[new DropdownSubmenu(formats, { label: 'Formats' })],
-
+ inlineStyles,
],
});
import {addListNodes} from "prosemirror-schema-list";
const baseNodes = addListNodes(basicSchema.spec.nodes, "paragraph block*", "block");
+const baseMarks = basicSchema.spec.marks;
const nodeCallout = {
attrs: {
{tag: 'p.callout.warning', attrs: {type: 'warning'}, priority: 75,},
{tag: 'p.callout', attrs: {type: 'info'}, priority: 75},
],
- toDOM: function(node) {
+ toDOM(node) {
const type = node.attrs.type || 'info';
return ['p', {class: 'callout ' + type}, 0];
}
};
+const markUnderline = {
+ parseDOM: [{tag: "u"}, {style: "text-decoration=underline"}],
+ toDOM() {
+ return ["span", {style: "text-decoration: underline;"}, 0];
+ }
+}
+
const customNodes = baseNodes.append({
callout: nodeCallout,
});
+const customMarks = baseMarks.append({
+ underline: markUnderline,
+});
+
const schema = new Schema({
nodes: customNodes,
- marks: basicSchema.spec.marks
+ marks: customMarks,
})
export default schema;
\ No newline at end of file
const renderDoc = document.implementation.createHTMLDocument();
renderDoc.body.appendChild(fragment);
return renderDoc.body.innerHTML;
+}
+
+/**
+ * @class KeyedMultiStack
+ * Holds many stacks, seperated via a key, with a simple
+ * interface to pop and push values to the stacks.
+ */
+export class KeyedMultiStack {
+
+ constructor() {
+ this.stack = {};
+ }
+
+ /**
+ * @param {String} key
+ * @return {undefined|*}
+ */
+ pop(key) {
+ if (Array.isArray(this.stack[key])) {
+ return this.stack[key].pop();
+ }
+ return undefined;
+ }
+
+ /**
+ * @param {String} key
+ * @param {*} value
+ */
+ push(key, value) {
+ if (this.stack[key] === undefined) {
+ this.stack[key] = [];
+ }
+
+ this.stack[key].push(value);
+ }
}
\ No newline at end of file
<div id="content" style="display: none;">
<h2>This is an editable block</h2>
- <p>Lorem ipsum dolor sit amet, <strong>consectetur adipisicing</strong> elit. Asperiores?</p>
+ <p>
+ Lorem ipsum dolor sit amet, <strong>consectetur adipisicing</strong> elit. Asperiores? <br>
+ Some <span style="text-decoration: underline">Underlined content</span> Lorem ipsum dolor sit amet.
+ </p>
<p><img src="/user_avatar.png" alt="Logo"></p>
<ul>
<li>Item A</li>