mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-27 16:41:53 +03:00
Got underline working in editor
Major step, since this is the first inline HTML element which needed advanced parsing out on the markdown side, since not commonmark supported.
This commit is contained in:
@ -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: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: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: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: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: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",
|
"build": "npm-run-all --parallel build:*:dev",
|
||||||
"production": "npm-run-all --parallel build:*:production",
|
"production": "npm-run-all --parallel build:*:production",
|
||||||
"dev": "npm-run-all --parallel watch livereload",
|
"dev": "npm-run-all --parallel watch livereload",
|
||||||
|
@ -11,6 +11,8 @@ class MarkdownView {
|
|||||||
|
|
||||||
this.textarea = target.appendChild(document.createElement("textarea"))
|
this.textarea = target.appendChild(document.createElement("textarea"))
|
||||||
this.textarea.value = markdown;
|
this.textarea.value = markdown;
|
||||||
|
this.textarea.style.width = '1000px';
|
||||||
|
this.textarea.style.height = '1000px';
|
||||||
}
|
}
|
||||||
|
|
||||||
get content() {
|
get content() {
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import schema from "./schema";
|
import schema from "./schema";
|
||||||
import markdownit from "markdown-it";
|
import markdownit from "markdown-it";
|
||||||
import {MarkdownParser, defaultMarkdownParser} from "prosemirror-markdown";
|
import {MarkdownParser, defaultMarkdownParser} from "prosemirror-markdown";
|
||||||
import {htmlToDoc} from "./util";
|
import {htmlToDoc, KeyedMultiStack} from "./util";
|
||||||
|
|
||||||
const tokens = defaultMarkdownParser.tokens;
|
const tokens = defaultMarkdownParser.tokens;
|
||||||
|
|
||||||
// This is really a placeholder on the object to allow the below
|
// These are really a placeholder on the object to allow the below
|
||||||
// parser.tokenHandlers.html_block hack to work as desired.
|
// parser.tokenHandlers.html_[block/inline] hacks to work as desired.
|
||||||
tokens.html_block = {block: "callout", noCloseToken: true};
|
tokens.html_block = {block: "callout", noCloseToken: true};
|
||||||
|
tokens.html_inline = {mark: "underline"};
|
||||||
|
|
||||||
const tokenizer = markdownit("commonmark", {html: true});
|
const tokenizer = markdownit("commonmark", {html: true});
|
||||||
const parser = new MarkdownParser(schema, tokenizer, tokens);
|
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) {
|
parser.tokenHandlers.html_block = function(state, tok, tokens, i) {
|
||||||
const contentDoc = htmlToDoc(tok.content || '');
|
const contentDoc = htmlToDoc(tok.content || '');
|
||||||
for (const node of contentDoc.content.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('<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;
|
export default parser;
|
@ -5,10 +5,20 @@ const nodes = defaultMarkdownSerializer.nodes;
|
|||||||
const marks = defaultMarkdownSerializer.marks;
|
const marks = defaultMarkdownSerializer.marks;
|
||||||
|
|
||||||
nodes.callout = function(state, node) {
|
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] });
|
const html = docToHtml({ content: [node] });
|
||||||
state.write(html);
|
state.write(html);
|
||||||
state.closeBlock();
|
state.closeBlock();
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
const serializer = new MarkdownSerializer(nodes, marks);
|
const serializer = new MarkdownSerializer(nodes, marks);
|
||||||
|
|
||||||
|
@ -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 {
|
import {
|
||||||
MenuItem, Dropdown, DropdownSubmenu, renderGrouped, icons, joinUpItem, liftItem, selectParentNodeItem,
|
MenuItem, Dropdown, DropdownSubmenu, renderGrouped, icons, joinUpItem, liftItem, selectParentNodeItem,
|
||||||
undoItem, redoItem, wrapItem, blockTypeItem
|
undoItem, redoItem, wrapItem, blockTypeItem
|
||||||
@ -62,6 +55,7 @@ function markItem(markType, options) {
|
|||||||
const inlineStyles = [
|
const inlineStyles = [
|
||||||
markItem(schema.marks.strong, {title: "Bold", icon: icons.strong}),
|
markItem(schema.marks.strong, {title: "Bold", icon: icons.strong}),
|
||||||
markItem(schema.marks.em, {title: "Italic", icon: icons.em}),
|
markItem(schema.marks.em, {title: "Italic", icon: icons.em}),
|
||||||
|
markItem(schema.marks.underline, {title: "Underline", label: 'U'}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const formats = [
|
const formats = [
|
||||||
@ -109,9 +103,8 @@ const menu = menuBar({
|
|||||||
floating: false,
|
floating: false,
|
||||||
content: [
|
content: [
|
||||||
[undoItem, redoItem],
|
[undoItem, redoItem],
|
||||||
inlineStyles,
|
|
||||||
[new DropdownSubmenu(formats, { label: 'Formats' })],
|
[new DropdownSubmenu(formats, { label: 'Formats' })],
|
||||||
|
inlineStyles,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import {schema as basicSchema} from "prosemirror-schema-basic";
|
|||||||
import {addListNodes} from "prosemirror-schema-list";
|
import {addListNodes} from "prosemirror-schema-list";
|
||||||
|
|
||||||
const baseNodes = addListNodes(basicSchema.spec.nodes, "paragraph block*", "block");
|
const baseNodes = addListNodes(basicSchema.spec.nodes, "paragraph block*", "block");
|
||||||
|
const baseMarks = basicSchema.spec.marks;
|
||||||
|
|
||||||
const nodeCallout = {
|
const nodeCallout = {
|
||||||
attrs: {
|
attrs: {
|
||||||
@ -18,19 +19,30 @@ const nodeCallout = {
|
|||||||
{tag: 'p.callout.warning', attrs: {type: 'warning'}, priority: 75,},
|
{tag: 'p.callout.warning', attrs: {type: 'warning'}, priority: 75,},
|
||||||
{tag: 'p.callout', attrs: {type: 'info'}, priority: 75},
|
{tag: 'p.callout', attrs: {type: 'info'}, priority: 75},
|
||||||
],
|
],
|
||||||
toDOM: function(node) {
|
toDOM(node) {
|
||||||
const type = node.attrs.type || 'info';
|
const type = node.attrs.type || 'info';
|
||||||
return ['p', {class: 'callout ' + type}, 0];
|
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({
|
const customNodes = baseNodes.append({
|
||||||
callout: nodeCallout,
|
callout: nodeCallout,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const customMarks = baseMarks.append({
|
||||||
|
underline: markUnderline,
|
||||||
|
});
|
||||||
|
|
||||||
const schema = new Schema({
|
const schema = new Schema({
|
||||||
nodes: customNodes,
|
nodes: customNodes,
|
||||||
marks: basicSchema.spec.marks
|
marks: customMarks,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default schema;
|
export default schema;
|
@ -13,4 +13,39 @@ export function docToHtml(doc) {
|
|||||||
const renderDoc = document.implementation.createHTMLDocument();
|
const renderDoc = document.implementation.createHTMLDocument();
|
||||||
renderDoc.body.appendChild(fragment);
|
renderDoc.body.appendChild(fragment);
|
||||||
return renderDoc.body.innerHTML;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
@ -11,7 +11,10 @@
|
|||||||
|
|
||||||
<div id="content" style="display: none;">
|
<div id="content" style="display: none;">
|
||||||
<h2>This is an editable block</h2>
|
<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>
|
<p><img src="/user_avatar.png" alt="Logo"></p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Item A</li>
|
<li>Item A</li>
|
||||||
|
Reference in New Issue
Block a user