Page MenuHomeMusing Studio

No OneTemporary

diff --git a/prose/markdownParser.js b/prose/markdownParser.js
index 8b2b0c1..a5e2b3c 100644
--- a/prose/markdownParser.js
+++ b/prose/markdownParser.js
@@ -1,72 +1,79 @@
import { MarkdownParser } from "prosemirror-markdown";
import markdownit from "markdown-it";
import { writeFreelySchema } from "./schema";
const md = markdownit("commonmark", { html: true });
-// Re-type <!--more--> html_block tokens so they can be handled distinctly
-// from other HTML blocks.
-md.core.ruler.push("readmore", (state) => {
+// Map HTML comment shortcodes to their own token types so they are handled
+// as special blocks rather than generic HTML.
+const SHORTCODE_TOKENS = {
+ "<!--more-->": "readmore_block",
+ "<!--emailsub-->": "emailsub_block",
+};
+
+md.core.ruler.push("shortcodes", (state) => {
for (let i = 0; i < state.tokens.length; i++) {
const token = state.tokens[i];
- if (token.type === "html_block" && token.content.trim() === "<!--more-->") {
- token.type = "readmore_block";
+ if (token.type === "html_block") {
+ const mapped = SHORTCODE_TOKENS[token.content.trim()];
+ if (mapped) token.type = mapped;
}
}
});
export const writeFreelyMarkdownParser = new MarkdownParser(
writeFreelySchema,
md,
{
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" },
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 },
readmore_block: { node: "readmore" },
+ emailsub_block: { node: "emailsub" },
html_block: {
node: "html_block",
getAttrs: (tok) => ({ content: tok.content }),
},
html_inline: {
node: "html_inline",
getAttrs: (tok) => ({ content: tok.content }),
},
}
);
diff --git a/prose/markdownSerializer.js b/prose/markdownSerializer.js
index 61f54ee..3e0175b 100644
--- a/prose/markdownSerializer.js
+++ b/prose/markdownSerializer.js
@@ -1,135 +1,139 @@
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);
},
+ emailsub(state, node) {
+ state.write("<!--emailsub-->\n");
+ state.closeBlock(node);
+ },
html_block(state, node) {
state.write(node.attrs.content);
state.closeBlock(node);
},
html_inline(state, node) {
state.write(node.attrs.content);
},
blockquote(state, node) {
state.wrapBlock("> ", null, 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/schema.js b/prose/schema.js
index bb32112..1d66123 100644
--- a/prose/schema.js
+++ b/prose/schema.js
@@ -1,40 +1,55 @@
import { schema } from "prosemirror-markdown";
import { Schema } from "prosemirror-model";
export const writeFreelySchema = new Schema({
nodes: schema.spec.nodes
.addToEnd("readmore", {
inline: false,
content: "",
group: "block",
draggable: true,
toDOM: (node) => [
"div",
{ class: "editorreadmore" },
"Read more...",
],
parseDOM: [{ tag: "div.editorreadmore" }],
})
+ .addToEnd("emailsub", {
+ inline: false,
+ content: "",
+ group: "block",
+ draggable: true,
+ toDOM: () => [
+ "div", { id: "emailsub", contenteditable: "false" },
+ ["form", {},
+ ["p", {}, "Enter your email to subscribe to updates."],
+ ["input", { type: "email", name: "email", placeholder: "me@example.com" }],
+ ["input", { type: "submit", id: "subscribe-btn", value: "Subscribe" }],
+ ],
+ ],
+ parseDOM: [{ tag: "div#emailsub" }],
+ })
.addToEnd("html_block", {
attrs: { content: { default: "" } },
content: "",
group: "block",
marks: "",
draggable: true,
toDOM: (node) => [
"div",
{ class: "editor-html-block", contenteditable: "false" },
node.attrs.content,
],
parseDOM: [{ tag: "div.editor-html-block", getAttrs: (dom) => ({ content: dom.textContent }) }],
})
.addToEnd("html_inline", {
attrs: { content: { default: "" } },
inline: true,
content: "",
group: "inline",
toDOM: (node) => ["code", { class: "editor-html-inline" }, node.attrs.content],
parseDOM: [{ tag: "code.editor-html-inline", getAttrs: (dom) => ({ content: dom.textContent }) }],
}),
marks: schema.spec.marks,
});

File Metadata

Mime Type
text/x-diff
Expires
Sun, May 17, 1:39 AM (10 h, 4 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3732171

Event Timeline