diff --git a/package.json b/package.json
index fdf65f63b..c4ea1a302 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,8 @@
"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",
diff --git a/resources/js/editor/MarkdownView.js b/resources/js/editor/MarkdownView.js
index f952c8606..88fc1fff5 100644
--- a/resources/js/editor/MarkdownView.js
+++ b/resources/js/editor/MarkdownView.js
@@ -11,6 +11,8 @@ class MarkdownView {
this.textarea = target.appendChild(document.createElement("textarea"))
this.textarea.value = markdown;
+ this.textarea.style.width = '1000px';
+ this.textarea.style.height = '1000px';
}
get content() {
diff --git a/resources/js/editor/markdown-parser.js b/resources/js/editor/markdown-parser.js
index 46495a7e0..b198ffad4 100644
--- a/resources/js/editor/markdown-parser.js
+++ b/resources/js/editor/markdown-parser.js
@@ -1,17 +1,20 @@
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) {
@@ -19,4 +22,44 @@ parser.tokenHandlers.html_block = function(state, tok, tokens, i) {
}
};
+// 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('
' + (html || '') + '
');
+ 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
diff --git a/resources/js/editor/markdown-serializer.js b/resources/js/editor/markdown-serializer.js
index 8e1e2b816..a7c5d7d82 100644
--- a/resources/js/editor/markdown-serializer.js
+++ b/resources/js/editor/markdown-serializer.js
@@ -5,10 +5,20 @@ const nodes = defaultMarkdownSerializer.nodes;
const marks = defaultMarkdownSerializer.marks;
nodes.callout = function(state, node) {
+ writeNodeAsHtml(state, node);
+};
+
+marks.underline = {
+ open: '',
+ close: '',
+};
+
+function writeNodeAsHtml(state, node) {
const html = docToHtml({ content: [node] });
state.write(html);
state.closeBlock();
-};
+}
+
const serializer = new MarkdownSerializer(nodes, marks);
diff --git a/resources/js/editor/menu/index.js b/resources/js/editor/menu/index.js
index 1bdc718dc..591878f7c 100644
--- a/resources/js/editor/menu/index.js
+++ b/resources/js/editor/menu/index.js
@@ -1,10 +1,3 @@
-/**
- * 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
@@ -62,6 +55,7 @@ function markItem(markType, options) {
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 = [
@@ -109,9 +103,8 @@ const menu = menuBar({
floating: false,
content: [
[undoItem, redoItem],
- inlineStyles,
[new DropdownSubmenu(formats, { label: 'Formats' })],
-
+ inlineStyles,
],
});
diff --git a/resources/js/editor/schema.js b/resources/js/editor/schema.js
index 53a08af1f..fb6192f22 100644
--- a/resources/js/editor/schema.js
+++ b/resources/js/editor/schema.js
@@ -3,6 +3,7 @@ import {schema as basicSchema} from "prosemirror-schema-basic";
import {addListNodes} from "prosemirror-schema-list";
const baseNodes = addListNodes(basicSchema.spec.nodes, "paragraph block*", "block");
+const baseMarks = basicSchema.spec.marks;
const nodeCallout = {
attrs: {
@@ -18,19 +19,30 @@ const nodeCallout = {
{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
diff --git a/resources/js/editor/util.js b/resources/js/editor/util.js
index ec940bd2b..3c9cffde5 100644
--- a/resources/js/editor/util.js
+++ b/resources/js/editor/util.js
@@ -13,4 +13,39 @@ export function docToHtml(doc) {
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
diff --git a/resources/views/editor-test.blade.php b/resources/views/editor-test.blade.php
index bba8c3eca..bba27f153 100644
--- a/resources/views/editor-test.blade.php
+++ b/resources/views/editor-test.blade.php
@@ -11,7 +11,10 @@
This is an editable block
-
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores?
+
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores?
+ Some Underlined content Lorem ipsum dolor sit amet.
+
