mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04: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:
parent
9d7174557e
commit
a8f48185b5
@ -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",
|
||||
|
@ -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() {
|
||||
|
@ -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('<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;
|
@ -5,10 +5,20 @@ const nodes = defaultMarkdownSerializer.nodes;
|
||||
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);
|
||||
|
||||
|
@ -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,
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
@ -11,7 +11,10 @@
|
||||
|
||||
<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>
|
||||
|
Loading…
Reference in New Issue
Block a user