Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F14870718
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
8 KB
Subscribers
None
View Options
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(
`}${
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
Details
Attached
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
Attached To
rWF WriteFreely
Event Timeline
Log In to Comment