Page MenuHomeMusing Studio

No OneTemporary

diff --git a/less/prose-editor.less b/less/prose-editor.less
index 6a43c29..fc8e8ba 100644
--- a/less/prose-editor.less
+++ b/less/prose-editor.less
@@ -1,451 +1,452 @@
body#pad.classic {
header {
display: flex;
justify-content: space-between;
align-items: center;
}
#editor {
top: 4em;
}
#title {
top: 4.25rem;
bottom: unset;
height: auto;
font-weight: bold;
font-size: 2em;
padding-top: 0;
padding-bottom: 0;
border: 0;
}
#tools {
#belt {
float: none;
}
}
#target {
ul {
a {
padding: 0 0.5em !important;
}
}
}
}
.ProseMirror {
position: relative;
height: calc(~"100% - 1.6em");
overflow-y: auto;
box-sizing: border-box;
-moz-box-sizing: border-box;
font-size: 1.2em;
word-wrap: break-word;
white-space: pre-wrap;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
padding: 0.5em 0;
line-height: 1.5;
outline: none;
}
.ProseMirror pre {
white-space: pre-wrap;
}
.ProseMirror li {
position: relative;
}
.ProseMirror-hideselection *::selection {
background: transparent;
}
.ProseMirror-hideselection *::-moz-selection {
background: transparent;
}
.ProseMirror-hideselection {
caret-color: transparent;
}
.ProseMirror-selectednode {
outline: 2px solid #8cf;
}
/* Make sure li selections wrap around markers */
li.ProseMirror-selectednode {
outline: none;
}
li.ProseMirror-selectednode:after {
content: "";
position: absolute;
left: -32px;
right: -2px;
top: -2px;
bottom: -2px;
border: 2px solid #8cf;
pointer-events: none;
}
.ProseMirror-textblock-dropdown {
min-width: 3em;
}
.ProseMirror-menu {
margin: 0 -4px;
line-height: 1;
}
.ProseMirror-tooltip .ProseMirror-menu {
width: -webkit-fit-content;
width: fit-content;
white-space: pre;
}
.ProseMirror-menuitem {
margin-right: 3px;
display: inline-block;
div {
cursor: pointer;
}
}
.ProseMirror-menuseparator {
border-right: 1px solid #ddd;
margin-right: 3px;
}
.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
font-size: 90%;
white-space: nowrap;
}
.ProseMirror-menu-dropdown {
vertical-align: 1px;
cursor: pointer;
position: relative;
padding-right: 15px;
}
.ProseMirror-menu-dropdown-wrap {
padding: 1px 0 1px 4px;
display: inline-block;
position: relative;
}
.ProseMirror-menu-dropdown:after {
content: "";
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid currentColor;
opacity: .6;
position: absolute;
right: 4px;
top: calc(50% - 2px);
}
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
position: absolute;
background: white;
color: #666;
border: 1px solid #aaa;
padding: 2px;
}
.ProseMirror-menu-dropdown-menu {
z-index: 15;
min-width: 6em;
}
.ProseMirror-menu-dropdown-item {
cursor: pointer;
padding: 2px 8px 2px 4px;
}
.ProseMirror-menu-dropdown-item:hover {
background: #f2f2f2;
}
.ProseMirror-menu-submenu-wrap {
position: relative;
margin-right: -4px;
}
.ProseMirror-menu-submenu-label:after {
content: "";
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 4px solid currentColor;
opacity: .6;
position: absolute;
right: 4px;
top: calc(50% - 4px);
}
.ProseMirror-menu-submenu {
display: none;
min-width: 4em;
left: 100%;
top: -3px;
}
.ProseMirror-menu-active {
background: #eee;
border-radius: 4px;
}
.ProseMirror-menu-active {
background: #eee;
border-radius: 4px;
}
.ProseMirror-menu-disabled {
opacity: .3;
}
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
display: block;
}
.ProseMirror-menubar {
position: relative;
min-height: 1em;
color: #666;
padding: 0.5em;
top: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.8);
z-index: 10;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow: visible;
}
.ProseMirror-icon {
display: inline-block;
line-height: .8;
vertical-align: -2px; /* Compensate for padding */
padding: 2px 8px;
cursor: pointer;
}
.ProseMirror-menu-disabled.ProseMirror-icon {
cursor: default;
}
.ProseMirror-icon svg {
fill: currentColor;
height: 1em;
}
.ProseMirror-icon span {
vertical-align: text-top;
}
.ProseMirror-gapcursor {
display: none;
pointer-events: none;
position: absolute;
}
.ProseMirror-gapcursor:after {
content: "";
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid black;
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to {
visibility: hidden;
}
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}
/* Add space around the hr to make clicking it easier */
.ProseMirror-example-setup-style hr {
- padding: 2px 10px;
+ padding: 4px 10px;
border: none;
margin: 1em 0;
+ background: initial;
}
.ProseMirror-example-setup-style hr:after {
content: "";
display: block;
height: 1px;
- background-color: silver;
+ background-color: #ccc;
line-height: 2px;
}
.ProseMirror ul, .ProseMirror ol {
padding-left: 30px;
}
.ProseMirror blockquote {
padding-left: 1em;
border-left: 3px solid #eee;
margin-left: 0;
margin-right: 0;
}
.ProseMirror-example-setup-style img {
cursor: default;
max-width: 100%;
}
.ProseMirror-prompt {
background: white;
padding: 1em;
border: 1px solid silver;
position: fixed;
border-radius: 0.25em;
z-index: 11;
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
}
.ProseMirror-prompt h5 {
margin: 0 0 0.75em;
font-family: @sansFont;
font-size: 100%;
color: #444;
}
.ProseMirror-prompt input[type="text"],
.ProseMirror-prompt textarea {
background: #eee;
border: none;
outline: none;
}
.ProseMirror-prompt input[type="text"] {
margin: 0.25em 0;
}
.ProseMirror-prompt-close {
position: absolute;
left: 2px;
top: 1px;
color: #666;
border: none;
background: transparent;
padding: 0;
}
.ProseMirror-prompt-close:after {
content: "✕";
font-size: 12px;
}
.ProseMirror-invalid {
background: #ffc;
border: 1px solid #cc7;
border-radius: 4px;
padding: 5px 10px;
position: absolute;
min-width: 10em;
}
.ProseMirror-prompt-buttons {
margin-top: 5px;
display: none;
}
#editor, .editor {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
color: black;
background-clip: padding-box;
padding: 5px 0;
margin: 4em auto 23px auto;
}
.ProseMirror p:first-child,
.ProseMirror h1:first-child,
.ProseMirror h2:first-child,
.ProseMirror h3:first-child,
.ProseMirror h4:first-child,
.ProseMirror h5:first-child,
.ProseMirror h6:first-child {
margin-top: 10px;
}
.ProseMirror p {
margin-bottom: 1em;
}
textarea {
width: 100%;
height: 123px;
border: 1px solid silver;
box-sizing: border-box;
-moz-box-sizing: border-box;
padding: 3px 10px;
border: none;
outline: none;
font-family: inherit;
font-size: inherit;
}
.ProseMirror-menubar-wrapper {
height: 100%;
box-sizing: border-box;
}
.ProseMirror-menubar-wrapper, #markdown textarea {
display: block;
margin-bottom: 4px;
}
.editorreadmore {
color: @textLinkColor;
text-decoration: underline;
text-align: center;
width: 100%;
}
@media all and (min-width: 50em) {
#editor {
margin-left: 10%;
margin-right: 10%;
}
}
@media all and (min-width: 60em) {
#editor {
margin-left: 15%;
margin-right: 15%;
}
}
@media all and (min-width: 70em) {
#editor {
margin-left: 20%;
margin-right: 20%;
}
}
@media all and (min-width: 85em) {
#editor {
margin-left: 25%;
margin-right: 25%;
}
}
@media all and (min-width: 105em) {
#editor {
margin-left: 30%;
margin-right: 30%;
}
}
diff --git a/prose/markdownParser.js b/prose/markdownParser.js
index bd81590..1ee5a07 100644
--- a/prose/markdownParser.js
+++ b/prose/markdownParser.js
@@ -1,57 +1,57 @@
import { MarkdownParser } from "prosemirror-markdown";
import markdownit from "markdown-it";
import { writeFreelySchema } from "./schema";
export const writeFreelyMarkdownParser = 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" },
+ hr: { node: "horizontal_rule" },
image: {
node: "image",
getAttrs: (tok) => ({
src: tok.attrGet("src"),
title: tok.attrGet("title") || null,
alt: (tok.children !== null && typeof tok.children[0] !== 'undefined' ? 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 {};
},
},
}
);
diff --git a/prose/markdownSerializer.js b/prose/markdownSerializer.js
index d14deab..9615633 100644
--- a/prose/markdownSerializer.js
+++ b/prose/markdownSerializer.js
@@ -1,124 +1,128 @@
import { MarkdownSerializer } from "prosemirror-markdown";
function backticksFor(node, side) {
const ticks = /`+/g;
let m;
let len = 0;
if (node.isText)
while ((m = ticks.exec(node.text))) len = Math.max(len, m[0].length);
let result = len > 0 && side > 0 ? " `" : "`";
for (let i = 0; i < len; i++) result += "`";
if (len > 0 && side < 0) result += " ";
return result;
}
function isPlainURL(link, parent, index, side) {
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false;
const content = parent.child(index + (side < 0 ? -1 : 0));
if (
!content.isText ||
content.text != link.attrs.href ||
content.marks[content.marks.length - 1] != link
)
return false;
if (index == (side < 0 ? 1 : parent.childCount - 1)) return true;
const next = parent.child(index + (side < 0 ? -2 : 1));
return !link.isInSet(next.marks);
}
export const writeFreelyMarkdownSerializer = new MarkdownSerializer(
{
readmore(state, node) {
state.write("<!--more-->\n");
state.closeBlock(node);
},
// blockquote(state, node) {
// state.wrapBlock("> ", undefined, node, () => state.renderContent(node));
// },
code_block(state, node) {
state.write(`\`\`\`${node.attrs.params || ""}\n`);
state.text(node.textContent, false);
state.ensureNewLine();
state.write("```");
state.closeBlock(node);
},
heading(state, node) {
state.write(`${state.repeat("#", node.attrs.level)} `);
state.renderInline(node);
state.closeBlock(node);
},
+ horizontal_rule: function horizontal_rule(state, node) {
+ state.write(node.attrs.markup || "---");
+ state.closeBlock(node);
+ },
bullet_list(state, node) {
node.attrs.tight = true;
state.renderList(node, " ", () => `${node.attrs.bullet || "*"} `);
},
ordered_list(state, node) {
const start = node.attrs.order || 1;
const maxW = String(start + node.childCount - 1).length;
const space = state.repeat(" ", maxW + 2);
state.renderList(node, space, (i) => {
const nStr = String(start + i);
return `${state.repeat(" ", maxW - nStr.length) + nStr}. `;
});
},
list_item(state, node) {
state.renderContent(node);
},
paragraph(state, node) {
state.renderInline(node);
state.closeBlock(node);
},
image(state, node) {
state.write(
`![${state.esc(node.attrs.alt || "")}](${state.esc(node.attrs.src)}${
node.attrs.title ? ` ${state.quote(node.attrs.title)}` : ""
})`
);
},
hard_break(state, node, parent, index) {
for (let i = index + 1; i < parent.childCount; i += 1)
if (parent.child(i).type !== node.type) {
state.write("\\\n");
return;
}
},
text(state, node) {
state.text(node.text || "");
},
},
{
em: {
open: "*",
close: "*",
mixable: true,
expelEnclosingWhitespace: true,
},
strong: {
open: "**",
close: "**",
mixable: true,
expelEnclosingWhitespace: true,
},
link: {
open(_state, mark, parent, index) {
return isPlainURL(mark, parent, index, 1) ? "<" : "[";
},
close(state, mark, parent, index) {
return isPlainURL(mark, parent, index, -1)
? ">"
: `](${state.esc(mark.attrs.href)}${
mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ""
})`;
},
},
code: {
open(_state, _mark, parent, index) {
return backticksFor(parent.child(index), -1);
},
close(_state, _mark, parent, index) {
return backticksFor(parent.child(index - 1), 1);
},
escape: false,
},
}
);
diff --git a/prose/prose.js b/prose/prose.js
index 7c361a0..3096c9e 100644
--- a/prose/prose.js
+++ b/prose/prose.js
@@ -1,118 +1,118 @@
// class MarkdownView {
// constructor(target, content) {
// this.textarea = target.appendChild(document.createElement("textarea"))
// this.textarea.value = content
// }
// get content() { return this.textarea.value }
// focus() { this.textarea.focus() }
// destroy() { this.textarea.remove() }
// }
import { EditorView } from "prosemirror-view";
import { EditorState, TextSelection } from "prosemirror-state";
import { exampleSetup } from "prosemirror-example-setup";
import { keymap } from "prosemirror-keymap";
import { writeFreelyMarkdownParser } from "./markdownParser";
import { writeFreelyMarkdownSerializer } from "./markdownSerializer";
import { writeFreelySchema } from "./schema";
import { getMenu } from "./menu";
let $title = document.querySelector("#title");
let $content = document.querySelector("#content");
// 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 )
class ProseMirrorView {
constructor(target, content) {
let typingTimer;
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;
}
const doc = writeFreelyMarkdownParser.parse(
// Replace all "solo" \n's with \\\n for correct markdown parsing
// Can't use lookahead or lookbehind because it's not supported on Safari
content.replace(/([^]{0,1})(\n)([^]{0,1})/g, (match, p1, p2, p3) => {
return p1 !== "\n" && p3 !== "\n" ? p1 + "\\\n" + p3 : match;
})
);
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) {
let newState = this.state.apply(transaction);
const newContent = writeFreelyMarkdownSerializer
.serialize(newState.doc)
// Replace all \\\ns ( not followed by a \n ) with \n
.replace(/(\\\n)(\n{0,1})/g, (match, p1, p2) =>
p2 !== "\n" ? "\n" + p2 : match
);
$content.value = newContent;
let draft = "";
if ($title.value != null && $title.value !== "") {
draft = "# " + $title.value + "\n\n";
}
draft += newContent;
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
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);
+ return writeFreelyMarkdownSerializer.serialize(this.view.state.doc);
}
focus() {
this.view.focus();
}
destroy() {
this.view.destroy();
}
}
let place = document.querySelector("#editor");
let view = new ProseMirrorView(place, $content.value);
diff --git a/prose/schema.js b/prose/schema.js
index 67302df..0a87662 100644
--- a/prose/schema.js
+++ b/prose/schema.js
@@ -1,21 +1,20 @@
import { schema } from "prosemirror-markdown";
import { Schema } from "prosemirror-model";
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" },
"Read more...",
],
parseDOM: [{ tag: "div.editorreadmore" }],
}),
marks: schema.spec.marks,
});

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jan 20, 3:43 AM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3137609

Event Timeline