@october/slate-md-serializer
Advanced tools
Comparing version 1.0.26 to 1.0.27
@@ -58,10 +58,10 @@ "use strict"; | ||
fences: noop, | ||
hr: /^( *[-*_]){3,} *(?:\n+|$)/, | ||
heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, | ||
hr: /^( *[-*_]){3,} *(?:\n|$)/, | ||
heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n|$)/, | ||
nptable: noop, | ||
lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, | ||
blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, | ||
list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, | ||
def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, | ||
paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|def))+)\n*/, | ||
lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n|$)/, | ||
blockquote: /^( *>[^\n]+(\n(?!def)[^\n])*(?:\n|$))+/, | ||
list: /^( *)(bull) [\s\S]+?(?:hr|def|\n(?! )(?!\1bull )\n|\s*$)/, | ||
def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n|$)/, | ||
paragraph: /^((?:[^\n]+(?!hr|heading|lheading|blockquote|def))+)(?:\n|$)/, | ||
text: /^[^\n]+/ | ||
@@ -91,5 +91,5 @@ }; | ||
block.gfm = assign({}, block.normal, { | ||
fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, | ||
fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n|$)/, | ||
paragraph: /^/, | ||
heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/ | ||
heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n{1,2}|$)/ | ||
}); | ||
@@ -104,4 +104,4 @@ | ||
block.tables = assign({}, block.gfm, { | ||
nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, | ||
table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ | ||
nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)/, | ||
table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)/ | ||
}); | ||
@@ -169,2 +169,3 @@ | ||
src = src.replace(/^ +$/gm, ""); | ||
src = src.replace(/^\n/, ""); | ||
@@ -175,6 +176,11 @@ while (src) { | ||
src = src.substring(cap[0].length); | ||
if (cap[0].length > 1) { | ||
this.tokens.push({ | ||
type: "space" | ||
}); | ||
var newlines = cap[0].length; | ||
if (top) { | ||
for (var _i = 0; _i < newlines; _i++) { | ||
this.tokens.push({ | ||
type: "paragraph", | ||
text: "" | ||
}); | ||
} | ||
} | ||
@@ -208,2 +214,8 @@ } | ||
src = src.substring(cap[0].length); | ||
var last = this.tokens[this.tokens.length - 1]; | ||
if (last && last.type === "paragraph" && last.text === "") { | ||
this.tokens.splice(-1, 1); | ||
} | ||
this.tokens.push({ | ||
@@ -412,6 +424,13 @@ type: "heading", | ||
src = src.substring(cap[0].length); | ||
var endsWithNewline = cap[1].charAt(cap[1].length - 1) === "\n"; | ||
this.tokens.push({ | ||
type: "paragraph", | ||
text: cap[1].charAt(cap[1].length - 1) === "\n" ? cap[1].slice(0, -1) : cap[1] | ||
text: endsWithNewline ? cap[1].slice(0, -1) : cap[1] | ||
}); | ||
if (endsWithNewline) { | ||
this.tokens.push({ | ||
type: "paragraph", | ||
text: "" | ||
}); | ||
} | ||
continue; | ||
@@ -554,3 +573,3 @@ } | ||
out.push({ | ||
kind: "text", | ||
object: "text", | ||
leaves: [{ | ||
@@ -580,3 +599,3 @@ text: cap[1] | ||
out.push({ | ||
kind: "text", | ||
object: "text", | ||
leaves: [{ | ||
@@ -678,13 +697,13 @@ text: cap[0].charAt(0) | ||
var accLast = acc.length - 1; | ||
var lastIsText = accLast >= 0 && acc[accLast] && acc[accLast]["kind"] === "text"; | ||
var lastIsText = accLast >= 0 && acc[accLast] && acc[accLast]["object"] === "text"; | ||
if (current instanceof TextNode) { | ||
if (current.text) { | ||
if (lastIsText) { | ||
// If the previous item was a text kind, push the current text to it's range | ||
// If the previous item was a text object, push the current text to it's range | ||
acc[accLast].leaves.push(current); | ||
return acc; | ||
} else { | ||
// Else, create a new text kind | ||
// Else, create a new text object | ||
acc.push({ | ||
kind: "text", | ||
object: "text", | ||
leaves: [current] | ||
@@ -711,3 +730,3 @@ }); | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "code", | ||
@@ -721,3 +740,3 @@ data: data, | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "block-quote", | ||
@@ -730,3 +749,3 @@ nodes: this.groupTextInLeaves(childNode) | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "heading" + level, | ||
@@ -739,10 +758,4 @@ nodes: this.groupTextInLeaves(childNode) | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "horizontal-rule", | ||
nodes: [{ | ||
kind: "text", | ||
leaves: [{ | ||
text: "" | ||
}] | ||
}], | ||
isVoid: true | ||
@@ -754,3 +767,3 @@ }; | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: style + "-list", | ||
@@ -770,3 +783,3 @@ nodes: childNode | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "list-item", | ||
@@ -780,3 +793,3 @@ data: data, | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "paragraph", | ||
@@ -789,3 +802,3 @@ nodes: this.groupTextInLeaves(childNode) | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "table", | ||
@@ -798,3 +811,3 @@ nodes: childNode | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "table-row", | ||
@@ -809,3 +822,3 @@ nodes: childNode | ||
return { | ||
kind: "block", | ||
object: "block", | ||
data: { align: align }, | ||
@@ -841,7 +854,12 @@ type: flags.header ? "table-head" : "table-cell", | ||
Renderer.prototype.codespan = function (text) { | ||
return new TextNode(text, { type: "code" }); | ||
return { | ||
text: text, | ||
marks: [{ type: "code" }] | ||
}; | ||
}; | ||
Renderer.prototype.br = function () { | ||
return new TextNode(""); | ||
return { | ||
text: " " | ||
}; | ||
}; | ||
@@ -879,3 +897,3 @@ | ||
return { | ||
kind: "inline", | ||
object: "inline", | ||
type: "link", | ||
@@ -900,6 +918,6 @@ nodes: this.groupTextInLeaves(childNode), | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "image", | ||
nodes: [{ | ||
kind: "text", | ||
object: "text", | ||
leaves: [{ | ||
@@ -915,13 +933,7 @@ text: "" | ||
Renderer.prototype.text = function (childNode) { | ||
return new TextNode(childNode); | ||
return { | ||
text: childNode | ||
}; | ||
}; | ||
// Auxiliary object constructors: | ||
function TextNode(text, marks) { | ||
this.text = text; | ||
if (marks) { | ||
this.marks = [marks]; | ||
} | ||
} | ||
/** | ||
@@ -1004,3 +1016,3 @@ * Parsing & Compiling | ||
return { | ||
kind: "text", | ||
object: "text", | ||
leaves: [{ | ||
@@ -1121,15 +1133,40 @@ text: "" | ||
options = assign({}, defaults, options); | ||
var fragment = void 0; | ||
try { | ||
var fragment = Parser.parse(Lexer.parse(src, options), options); | ||
} catch (e) { | ||
fragment = Parser.parse(Lexer.parse(src, options), options); | ||
if (!fragment.length) { | ||
fragment = [{ | ||
object: "block", | ||
type: "paragraph", | ||
isVoid: false, | ||
data: {}, | ||
nodes: [{ | ||
object: "text", | ||
leaves: [{ | ||
object: "leaf", | ||
text: "", | ||
marks: [] | ||
}] | ||
}] | ||
}]; | ||
} | ||
} catch (err) { | ||
if (options.silent) { | ||
fragment = [{ | ||
kind: "block", | ||
object: "block", | ||
type: "paragraph", | ||
isVoid: false, | ||
data: {}, | ||
nodes: [{ | ||
kind: "text", | ||
object: "text", | ||
leaves: [{ | ||
text: "An error occured:" | ||
object: "leaf", | ||
text: "An error occured:", | ||
marks: [] | ||
}, { | ||
text: e.message | ||
object: "leaf", | ||
text: e.message, | ||
marks: [] | ||
}] | ||
@@ -1139,7 +1176,7 @@ }] | ||
} else { | ||
throw e; | ||
throw err; | ||
} | ||
} | ||
var mainNode = { nodes: fragment }; | ||
return mainNode; | ||
return { nodes: fragment }; | ||
} | ||
@@ -1146,0 +1183,0 @@ }; |
@@ -26,3 +26,3 @@ "use strict"; | ||
var String = new _immutable.Record({ | ||
kind: "string", | ||
object: "string", | ||
text: "" | ||
@@ -32,11 +32,7 @@ }); | ||
function formatLinkBar(img, url, title, desc, domain) { | ||
return "%%%\n" + (img ? url + "\n" + img : url) + "\n" + title + "\n" + desc.replace(/\[(.*?)\]/g, "") + "\n" + domain + "\n%%%\n"; | ||
return "\n\n%%%\n" + (img ? url + "\n" + img : url) + "\n" + title + "\n" + desc.replace(/\[(.*?)\]/g, "") + "\n" + domain + "\n%%%\n"; | ||
} | ||
function formatSoftBreak(children) { | ||
return children.replace(/\n/g, " \n"); | ||
} | ||
/** | ||
* Rules to (de)serialize nodes.git pu | ||
* Rules to (de)serialize nodes. | ||
* | ||
@@ -50,4 +46,4 @@ * @type {Object} | ||
serialize: function serialize(obj, children) { | ||
if (obj.kind === "string") { | ||
return ("" + children).replace(/\\/g, "\\\\").replace(/!/g, "\\!").replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/%/g, "\\%"); | ||
if (obj.object === "string") { | ||
return children; | ||
} | ||
@@ -57,3 +53,3 @@ } | ||
serialize: function serialize(obj, children, document) { | ||
if (obj.kind !== "block") return; | ||
if (obj.object !== "block") return; | ||
var parent = document.getParent(obj.key); | ||
@@ -64,3 +60,5 @@ | ||
tableHeader = ""; | ||
return children; | ||
// trim removes trailing newline | ||
return children.trim(); | ||
case "table-head": | ||
@@ -93,18 +91,21 @@ { | ||
case "paragraph": | ||
if (parent.type === "list-item") { | ||
return formatSoftBreak(children); | ||
} else { | ||
return "\n" + formatSoftBreak(children) + "\n"; | ||
} | ||
return children; | ||
case "code": | ||
return "```\n" + children + "\n```\n"; | ||
return "```\n" + children + "\n```"; | ||
case "code-line": | ||
return children + "\n"; | ||
case "block-quote": | ||
return "> " + children + "\n"; | ||
return "> " + children; | ||
case "todo-list": | ||
case "bulleted-list": | ||
case "ordered-list": | ||
if (parent === document) { | ||
return "\n" + children; | ||
{ | ||
// root list | ||
if (parent === document) { | ||
return children; | ||
} | ||
// nested list | ||
return "\n" + children.replace(/\n+$/gm, "").replace(/^/gm, " "); | ||
} | ||
return "\n" + children.replace(/^/gm, " "); | ||
case "list-item": | ||
@@ -114,32 +115,30 @@ { | ||
case "ordered-list": | ||
return "1. " + formatSoftBreak(children) + "\n"; | ||
return "1. " + children + "\n"; | ||
case "todo-list": | ||
var checked = obj.getIn(["data", "checked"]); | ||
var box = checked ? "[x]" : "[ ]"; | ||
return box + " " + formatSoftBreak(children) + "\n"; | ||
return box + " " + children + "\n"; | ||
default: | ||
case "bulleted-list": | ||
return "* " + formatSoftBreak(children) + "\n"; | ||
return "* " + children + "\n"; | ||
} | ||
} | ||
case "heading1": | ||
return "# " + formatSoftBreak(children); | ||
return "# " + children + "\n"; | ||
case "heading2": | ||
return "## " + children; | ||
return "\n## " + children + "\n"; | ||
case "heading3": | ||
return "### " + children; | ||
return "\n### " + children + "\n"; | ||
case "heading4": | ||
return "#### " + children; | ||
return "\n#### " + children + "\n"; | ||
case "heading5": | ||
return "##### " + children; | ||
return "\n##### " + children + "\n"; | ||
case "heading6": | ||
return "###### " + children; | ||
case "heading6": | ||
return "###### " + children; | ||
return "\n###### " + children + "\n"; | ||
case "horizontal-rule": | ||
return "---\n"; | ||
return "---"; | ||
case "image": | ||
var alt = obj.getIn(["data", "alt"]); | ||
var alt = obj.getIn(["data", "alt"]) || ""; | ||
var src = (0, _urls.encode)(obj.getIn(["data", "src"]) || ""); | ||
return "![" + alt + "](" + src + ")\n"; | ||
return "![" + alt + "](" + src + ")"; | ||
case "linkbar": | ||
@@ -157,9 +156,7 @@ var img = (0, _urls.encode)(obj.getIn(["data", "image"]) || ""); | ||
serialize: function serialize(obj, children) { | ||
if (obj.kind !== "inline") return; | ||
if (obj.object !== "inline") return; | ||
switch (obj.type) { | ||
case "link": | ||
var href = (0, _urls.encode)(obj.getIn(["data", "href"]) || ""); | ||
return "[" + children.trim() + "](" + href + ")"; | ||
case "code-line": | ||
return "`" + children + "`"; | ||
return href ? "[" + children.trim() + "](" + href + ")" : children.trim(); | ||
case "mention": | ||
@@ -175,3 +172,3 @@ var username = obj.getIn(["data", "username"]) || ""; | ||
serialize: function serialize(obj, children) { | ||
if (obj.kind !== "mark") return; | ||
if (obj.object !== "mark") return; | ||
switch (obj.type) { | ||
@@ -181,7 +178,7 @@ case "bold": | ||
case "italic": | ||
return "*" + children + "*"; | ||
return "_" + children + "_"; | ||
case "code": | ||
return "`" + children + "`"; | ||
case "inserted": | ||
return "__" + children + "__"; | ||
return "++" + children + "++"; | ||
case "deleted": | ||
@@ -256,5 +253,14 @@ return "~~" + children + "~~"; | ||
if (node.kind == "text") { | ||
if (node.object == "text") { | ||
var leaves = node.getLeaves(); | ||
return leaves.map(this.serializeLeaves); | ||
var inCodeBlock = !!document.getClosest(node.key, function (n) { | ||
return n.type === "code"; | ||
}); | ||
return leaves.map(function (leave) { | ||
var inCodeMark = !!leave.marks.filter(function (mark) { | ||
return mark.type === "code"; | ||
}).size; | ||
return _this2.serializeLeaves(leave, !inCodeBlock && !inCodeMark); | ||
}); | ||
} | ||
@@ -307,3 +313,10 @@ | ||
var string = new String({ text: leaves.text }); | ||
var escape = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; | ||
var leavesText = leaves.text; | ||
if (escape) { | ||
// escape markdown characters | ||
leavesText = leavesText.replace(/([\\`*{}\[\]()#+\-.!_>]@%)/gi, "\\$1").replace(/\n/g, " \n"); // format softBreaks | ||
} | ||
var string = new String({ text: leavesText }); | ||
var text = this.serializeString(string); | ||
@@ -310,0 +323,0 @@ |
@@ -10,7 +10,24 @@ "use strict"; | ||
function encode(href) { | ||
return href.trim().replace(/ /g, "%20").replace(/'/g, "%27").replace(/\(/g, "%28").replace(/\)/g, "%29"); | ||
return decodeSafe(href).trim().replace(/ /g, "%20").replace(/'/g, "%27").replace(/\(/g, "%28").replace(/\)/g, "%29"); | ||
} | ||
function decode(href) { | ||
return decodeURI(href); | ||
try { | ||
return decodeURI(href); | ||
} catch (e) { | ||
return decodeSafe(href); | ||
} | ||
} | ||
// convert hanging % characters into percentage encoded %25 as decodeURI cannot | ||
// handle this scenario but users may input 'invalid' urls. | ||
function decodeSafe(uri) { | ||
var components = uri.split(/(%(?:d0|d1)%.{2})/); | ||
return components.map(function (component) { | ||
try { | ||
return decodeURIComponent(component); | ||
} catch (e) { | ||
return component.replace(/%(?!\d+)/g, "%25"); | ||
} | ||
}).join(""); | ||
} |
{ | ||
"name": "@october/slate-md-serializer", | ||
"version": "1.0.26", | ||
"version": "1.0.27", | ||
"description": "", | ||
@@ -20,3 +20,3 @@ "main": "lib/renderer.js", | ||
"immutable": ">=3.0.0", | ||
"slate": ">=0.31.5" | ||
"slate": "~0.34.0" | ||
}, | ||
@@ -37,3 +37,3 @@ "devDependencies": { | ||
"react-dom": "^15.5.4", | ||
"slate": "0.31.5" | ||
"slate": "^0.34.5" | ||
}, | ||
@@ -40,0 +40,0 @@ "jest": { |
import MarkdownRenderer from "../renderer"; | ||
const Markdown = new MarkdownRenderer(); | ||
// By parsing, rendering and reparsing we can test both sides of the serializer | ||
// at the same time and ensure that parsing / rendering is compatible. | ||
function getNodes(text) { | ||
const parsed = Markdown.deserialize(text); | ||
const rendered = Markdown.serialize(parsed); | ||
const reparsed = Markdown.deserialize(rendered); | ||
return reparsed.document.nodes; | ||
} | ||
test("parses paragraph", () => { | ||
const output = Markdown.deserialize("This is just a sentance"); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
const text = "This is just a sentance"; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses two paragraphs", () => { | ||
const text = ` | ||
This is the first sentance | ||
This is the second sentance | ||
`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses two paragraphs", () => { | ||
const text = ` | ||
This is the first sentance | ||
This is the second sentance | ||
`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("maintains multiple empty paragraphs", () => { | ||
const text = ` | ||
This is the first sentance | ||
Two empty paragraphs above | ||
`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses heading1", () => { | ||
@@ -39,2 +75,59 @@ const output = Markdown.deserialize("# Heading"); | ||
test("headings are not greedy about newlines", () => { | ||
const text = ` | ||
a paragraph | ||
## Heading | ||
another paragraph | ||
`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses horizontal rule", () => { | ||
const text = ` | ||
--- | ||
a paragraph | ||
`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("bold mark", () => { | ||
const text = `**this is bold**`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("italic mark", () => { | ||
const text = `*this is italic* _this is italic too_`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("deleted mark", () => { | ||
const text = `~~this is strikethrough~~`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("inserted mark", () => { | ||
const text = `++inserted text++`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("code mark", () => { | ||
const text = "`const foo = 123;`"; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("code mark with escaped characters", () => { | ||
const text = "`<script>alert('foo')</script>`"; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("does not escape characters inside of code marks", () => { | ||
const text = "`<script>alert('foo')</script>`"; | ||
const parsed = Markdown.deserialize(text); | ||
const rendered = Markdown.serialize(parsed); | ||
expect(rendered).toMatchSnapshot(); | ||
}); | ||
test("parses quote", () => { | ||
@@ -44,14 +137,31 @@ const text = ` | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses quote with marks", () => { | ||
test("parses quote followed by list with quote (outline/#723)", () => { | ||
const text = ` | ||
> **bold** in a quote | ||
> this is a quote | ||
1. > this is a list item | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("quotes do not get combined", () => { | ||
const text = ` | ||
> this is a quote | ||
> this is a different quote | ||
`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("quote is not greedy about newlines", () => { | ||
const text = ` | ||
> this is a quote | ||
this is a paragraph | ||
`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses list items", () => { | ||
@@ -62,14 +172,24 @@ const text = ` | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses list with trailing item", () => { | ||
test("parses nested list items", () => { | ||
const text = ` | ||
- one | ||
- two | ||
- | ||
* one | ||
* two | ||
* nested | ||
next para`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("does not add extra paragraphs around lists", () => { | ||
const text = ` | ||
first paragraph | ||
- list | ||
second paragraph | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -82,4 +202,3 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -92,4 +211,3 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -102,4 +220,3 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -112,4 +229,3 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -123,4 +239,3 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -136,4 +251,3 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -149,7 +263,14 @@ | ||
`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
const output = Markdown.deserialize(text); | ||
const result = Markdown.serialize(output); | ||
const output2 = Markdown.deserialize(result); | ||
expect(output2.document.nodes).toMatchSnapshot(); | ||
test("tables are not greedy about newlines", () => { | ||
const text = ` | ||
| Tables | Are | Cool | | ||
|----------|:-------------:|------:| | ||
| col 1 is | left-aligned | $1600 | | ||
a new paragraph | ||
`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -162,4 +283,3 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -173,4 +293,3 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -186,4 +305,3 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -196,4 +314,3 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -210,6 +327,35 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("does not escape characters inside of code blocks", () => { | ||
const text = ` | ||
\`\`\` | ||
const hello = 'world'; | ||
function() { | ||
return hello; | ||
} | ||
\`\`\` | ||
`; | ||
const parsed = Markdown.deserialize(text); | ||
const rendered = Markdown.serialize(parsed); | ||
expect(rendered).toMatchSnapshot(); | ||
}); | ||
test("code is not greedy about newlines", () => { | ||
const text = ` | ||
one sentance | ||
\`\`\` | ||
const hello = 'world'; | ||
function() { | ||
return hello; | ||
} | ||
\`\`\` | ||
two sentance | ||
`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses ~~~ code fences", () => { | ||
@@ -224,4 +370,3 @@ const text = ` | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -236,10 +381,13 @@ | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses image", () => { | ||
const text = `![example](http://example.com/logo.png)`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses link", () => { | ||
const text = `[google](http://google.com)`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -249,4 +397,3 @@ | ||
const text = `**[google](http://google.com)**`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
@@ -256,26 +403,34 @@ | ||
const text = `[kibana](https://example.com/app/kibana#/discover?_g=%28refreshInterval:%28%27$$hashKey%27:%27object:1596%27,display:%2710%20seconds%27,pause:!f,section:1,value:10000%29,time:%28from:now-15m,mode:quick,to:now%29%29&_a=%28columns:!%28metadata.step,message,metadata.attempt_f,metadata.tries_f,metadata.error_class,metadata.url%29,index:%27logs-%27,interval:auto,query:%28query_string:%28analyze_wildcard:!t,query:%27metadata.at:%20Stepper*%27%29%29,sort:!%28time,desc%29%29)`; | ||
const output = Markdown.deserialize(text); | ||
const result = Markdown.serialize(output); | ||
const output2 = Markdown.deserialize(result); | ||
expect(output2.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses interesting nesting", () => { | ||
const text = ` | ||
* List item that contains a blockquote with inline mark | ||
test("parses link with percent symbol", () => { | ||
const text = `[kibana](https://example.com/app/kibana#/visualize/edit/Requests-%)`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
>Blockquote with code \`mapStateToProps()\` | ||
`; | ||
const output = Markdown.deserialize(text); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
test("ignores empty link", () => { | ||
const text = `[empty]()`; | ||
expect(getNodes(text)).toMatchSnapshot(); | ||
}); | ||
test("parses empty string", () => { | ||
const output = Markdown.deserialize(""); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes("")).toMatchSnapshot(); | ||
}); | ||
test("parses whitespace string", () => { | ||
const output = Markdown.deserialize(" "); | ||
expect(output.document.nodes).toMatchSnapshot(); | ||
expect(getNodes(" ")).toMatchSnapshot(); | ||
}); | ||
test("handles escaped blocks", () => { | ||
expect(getNodes("\\# text")).toMatchSnapshot(); | ||
expect(getNodes("\\- text")).toMatchSnapshot(); | ||
expect(getNodes("\\* text")).toMatchSnapshot(); | ||
}); | ||
test("handles escaped marks", () => { | ||
expect(getNodes("this is \\*\\*not bold\\*\\*")).toMatchSnapshot(); | ||
expect(getNodes("this is \\*not italic\\*")).toMatchSnapshot(); | ||
expect(getNodes("this is \\[not\\]\\(a link\\)")).toMatchSnapshot(); | ||
expect(getNodes("this is \\!\\[not\\]\\(an image\\)")).toMatchSnapshot(); | ||
}); |
@@ -54,10 +54,10 @@ import { decode } from "./urls"; | ||
fences: noop, | ||
hr: /^( *[-*_]){3,} *(?:\n+|$)/, | ||
heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, | ||
hr: /^( *[-*_]){3,} *(?:\n|$)/, | ||
heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n|$)/, | ||
nptable: noop, | ||
lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, | ||
blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, | ||
list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, | ||
def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, | ||
paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|def))+)\n*/, | ||
lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n|$)/, | ||
blockquote: /^( *>[^\n]+(\n(?!def)[^\n])*(?:\n|$))+/, | ||
list: /^( *)(bull) [\s\S]+?(?:hr|def|\n(?! )(?!\1bull )\n|\s*$)/, | ||
def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n|$)/, | ||
paragraph: /^((?:[^\n]+(?!hr|heading|lheading|blockquote|def))+)(?:\n|$)/, | ||
text: /^[^\n]+/ | ||
@@ -96,5 +96,5 @@ }; | ||
block.gfm = assign({}, block.normal, { | ||
fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, | ||
fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n|$)/, | ||
paragraph: /^/, | ||
heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/ | ||
heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n{1,2}|$)/ | ||
}); | ||
@@ -116,4 +116,4 @@ | ||
block.tables = assign({}, block.gfm, { | ||
nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, | ||
table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ | ||
nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)/, | ||
table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)/ | ||
}); | ||
@@ -185,2 +185,3 @@ | ||
src = src.replace(/^ +$/gm, ""); | ||
src = src.replace(/^\n/, ""); | ||
@@ -191,6 +192,11 @@ while (src) { | ||
src = src.substring(cap[0].length); | ||
if (cap[0].length > 1) { | ||
this.tokens.push({ | ||
type: "space" | ||
}); | ||
const newlines = cap[0].length; | ||
if (top) { | ||
for (let i = 0; i < newlines; i++) { | ||
this.tokens.push({ | ||
type: "paragraph", | ||
text: "" | ||
}); | ||
} | ||
} | ||
@@ -224,2 +230,8 @@ } | ||
src = src.substring(cap[0].length); | ||
const last = this.tokens[this.tokens.length - 1]; | ||
if (last && last.type === "paragraph" && last.text === "") { | ||
this.tokens.splice(-1, 1); | ||
} | ||
this.tokens.push({ | ||
@@ -432,8 +444,13 @@ type: "heading", | ||
src = src.substring(cap[0].length); | ||
const endsWithNewline = cap[1].charAt(cap[1].length - 1) === "\n"; | ||
this.tokens.push({ | ||
type: "paragraph", | ||
text: cap[1].charAt(cap[1].length - 1) === "\n" | ||
? cap[1].slice(0, -1) | ||
: cap[1] | ||
text: endsWithNewline ? cap[1].slice(0, -1) : cap[1] | ||
}); | ||
if (endsWithNewline) { | ||
this.tokens.push({ | ||
type: "paragraph", | ||
text: "" | ||
}); | ||
} | ||
continue; | ||
@@ -579,3 +596,3 @@ } | ||
out.push({ | ||
kind: "text", | ||
object: "text", | ||
leaves: [ | ||
@@ -610,3 +627,3 @@ { | ||
out.push({ | ||
kind: "text", | ||
object: "text", | ||
leaves: [ | ||
@@ -711,13 +728,13 @@ { | ||
let lastIsText = | ||
accLast >= 0 && acc[accLast] && acc[accLast]["kind"] === "text"; | ||
accLast >= 0 && acc[accLast] && acc[accLast]["object"] === "text"; | ||
if (current instanceof TextNode) { | ||
if (current.text) { | ||
if (lastIsText) { | ||
// If the previous item was a text kind, push the current text to it's range | ||
// If the previous item was a text object, push the current text to it's range | ||
acc[accLast].leaves.push(current); | ||
return acc; | ||
} else { | ||
// Else, create a new text kind | ||
// Else, create a new text object | ||
acc.push({ | ||
kind: "text", | ||
object: "text", | ||
leaves: [current] | ||
@@ -744,3 +761,3 @@ }); | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "code", | ||
@@ -754,3 +771,3 @@ data, | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "block-quote", | ||
@@ -763,3 +780,3 @@ nodes: this.groupTextInLeaves(childNode) | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "heading" + level, | ||
@@ -772,14 +789,4 @@ nodes: this.groupTextInLeaves(childNode) | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "horizontal-rule", | ||
nodes: [ | ||
{ | ||
kind: "text", | ||
leaves: [ | ||
{ | ||
text: "" | ||
} | ||
] | ||
} | ||
], | ||
isVoid: true | ||
@@ -791,3 +798,3 @@ }; | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: `${style}-list`, | ||
@@ -805,3 +812,3 @@ nodes: childNode | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "list-item", | ||
@@ -815,3 +822,3 @@ data, | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "paragraph", | ||
@@ -824,3 +831,3 @@ nodes: this.groupTextInLeaves(childNode) | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "table", | ||
@@ -833,3 +840,3 @@ nodes: childNode | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "table-row", | ||
@@ -844,3 +851,3 @@ nodes: childNode | ||
return { | ||
kind: "block", | ||
object: "block", | ||
data: { align }, | ||
@@ -876,7 +883,12 @@ type: flags.header ? "table-head" : "table-cell", | ||
Renderer.prototype.codespan = function(text) { | ||
return new TextNode(text, { type: "code" }); | ||
return { | ||
text, | ||
marks: [{ type: "code" }] | ||
}; | ||
}; | ||
Renderer.prototype.br = function() { | ||
return new TextNode(""); | ||
return { | ||
text: " " | ||
}; | ||
}; | ||
@@ -914,3 +926,3 @@ | ||
return { | ||
kind: "inline", | ||
object: "inline", | ||
type: "link", | ||
@@ -935,7 +947,7 @@ nodes: this.groupTextInLeaves(childNode), | ||
return { | ||
kind: "block", | ||
object: "block", | ||
type: "image", | ||
nodes: [ | ||
{ | ||
kind: "text", | ||
object: "text", | ||
leaves: [ | ||
@@ -954,13 +966,7 @@ { | ||
Renderer.prototype.text = function(childNode) { | ||
return new TextNode(childNode); | ||
return { | ||
text: childNode | ||
}; | ||
}; | ||
// Auxiliary object constructors: | ||
function TextNode(text, marks) { | ||
this.text = text; | ||
if (marks) { | ||
this.marks = [marks]; | ||
} | ||
} | ||
/** | ||
@@ -1042,3 +1048,3 @@ * Parsing & Compiling | ||
return { | ||
kind: "text", | ||
object: "text", | ||
leaves: [ | ||
@@ -1163,19 +1169,50 @@ { | ||
options = assign({}, defaults, options); | ||
let fragment; | ||
try { | ||
var fragment = Parser.parse(Lexer.parse(src, options), options); | ||
} catch (e) { | ||
fragment = Parser.parse(Lexer.parse(src, options), options); | ||
if (!fragment.length) { | ||
fragment = [ | ||
{ | ||
object: "block", | ||
type: "paragraph", | ||
isVoid: false, | ||
data: {}, | ||
nodes: [ | ||
{ | ||
object: "text", | ||
leaves: [ | ||
{ | ||
object: "leaf", | ||
text: "", | ||
marks: [] | ||
} | ||
] | ||
} | ||
] | ||
} | ||
]; | ||
} | ||
} catch (err) { | ||
if (options.silent) { | ||
fragment = [ | ||
{ | ||
kind: "block", | ||
object: "block", | ||
type: "paragraph", | ||
isVoid: false, | ||
data: {}, | ||
nodes: [ | ||
{ | ||
kind: "text", | ||
object: "text", | ||
leaves: [ | ||
{ | ||
text: "An error occured:" | ||
object: "leaf", | ||
text: "An error occured:", | ||
marks: [] | ||
}, | ||
{ | ||
text: e.message | ||
object: "leaf", | ||
text: e.message, | ||
marks: [] | ||
} | ||
@@ -1188,7 +1225,7 @@ ] | ||
} else { | ||
throw e; | ||
throw err; | ||
} | ||
} | ||
let mainNode = { nodes: fragment }; | ||
return mainNode; | ||
return { nodes: fragment }; | ||
} | ||
@@ -1195,0 +1232,0 @@ }; |
@@ -7,3 +7,3 @@ import parser from "./parser"; | ||
const String = new Record({ | ||
kind: "string", | ||
object: "string", | ||
text: "" | ||
@@ -13,3 +13,5 @@ }); | ||
function formatLinkBar(img, url, title, desc, domain) { | ||
return `%%% | ||
return ` | ||
%%% | ||
${img ? `${url}\n${img}` : url} | ||
@@ -23,8 +25,4 @@ ${title} | ||
function formatSoftBreak(children) { | ||
return children.replace(/\n/g, " \n"); | ||
} | ||
/** | ||
* Rules to (de)serialize nodes.git pu | ||
* Rules to (de)serialize nodes. | ||
* | ||
@@ -39,9 +37,4 @@ * @type {Object} | ||
serialize(obj, children) { | ||
if (obj.kind === "string") { | ||
return `${children}` | ||
.replace(/\\/g, "\\\\") | ||
.replace(/!/g, "\\!") | ||
.replace(/\[/g, "\\[") | ||
.replace(/\]/g, "\\]") | ||
.replace(/%/g, "\\%"); | ||
if (obj.object === "string") { | ||
return children; | ||
} | ||
@@ -52,3 +45,3 @@ } | ||
serialize(obj, children, document) { | ||
if (obj.kind !== "block") return; | ||
if (obj.object !== "block") return; | ||
let parent = document.getParent(obj.key); | ||
@@ -59,3 +52,5 @@ | ||
tableHeader = ""; | ||
return children; | ||
// trim removes trailing newline | ||
return children.trim(); | ||
case "table-head": { | ||
@@ -87,51 +82,51 @@ switch (obj.getIn(["data", "align"])) { | ||
case "paragraph": | ||
if (parent.type === "list-item") { | ||
return formatSoftBreak(children); | ||
} else { | ||
return `\n${formatSoftBreak(children)}\n`; | ||
} | ||
return children; | ||
case "code": | ||
return `\`\`\`\n${children}\n\`\`\`\n`; | ||
return `\`\`\`\n${children}\n\`\`\``; | ||
case "code-line": | ||
return `${children}\n`; | ||
case "block-quote": | ||
return `> ${children}\n`; | ||
return `> ${children}`; | ||
case "todo-list": | ||
case "bulleted-list": | ||
case "ordered-list": | ||
case "ordered-list": { | ||
// root list | ||
if (parent === document) { | ||
return `\n${children}`; | ||
return children; | ||
} | ||
return `\n${children.replace(/^/gm, " ")}`; | ||
// nested list | ||
return `\n${children.replace(/\n+$/gm, "").replace(/^/gm, " ")}`; | ||
} | ||
case "list-item": { | ||
switch (parent.type) { | ||
case "ordered-list": | ||
return `1. ${formatSoftBreak(children)}\n`; | ||
return `1. ${children}\n`; | ||
case "todo-list": | ||
let checked = obj.getIn(["data", "checked"]); | ||
let box = checked ? "[x]" : "[ ]"; | ||
return `${box} ${formatSoftBreak(children)}\n`; | ||
return `${box} ${children}\n`; | ||
default: | ||
case "bulleted-list": | ||
return `* ${formatSoftBreak(children)}\n`; | ||
return `* ${children}\n`; | ||
} | ||
} | ||
case "heading1": | ||
return `# ${formatSoftBreak(children)}`; | ||
return `# ${children}\n`; | ||
case "heading2": | ||
return `## ${children}`; | ||
return `\n## ${children}\n`; | ||
case "heading3": | ||
return `### ${children}`; | ||
return `\n### ${children}\n`; | ||
case "heading4": | ||
return `#### ${children}`; | ||
return `\n#### ${children}\n`; | ||
case "heading5": | ||
return `##### ${children}`; | ||
return `\n##### ${children}\n`; | ||
case "heading6": | ||
return `###### ${children}`; | ||
case "heading6": | ||
return `###### ${children}`; | ||
return `\n###### ${children}\n`; | ||
case "horizontal-rule": | ||
return `---\n`; | ||
return `---`; | ||
case "image": | ||
const alt = obj.getIn(["data", "alt"]); | ||
const alt = obj.getIn(["data", "alt"]) || ""; | ||
const src = encode(obj.getIn(["data", "src"]) || ""); | ||
return `![${alt}](${src})\n`; | ||
return `![${alt}](${src})`; | ||
case "linkbar": | ||
@@ -150,9 +145,7 @@ const img = encode(obj.getIn(["data", "image"]) || ""); | ||
serialize(obj, children) { | ||
if (obj.kind !== "inline") return; | ||
if (obj.object !== "inline") return; | ||
switch (obj.type) { | ||
case "link": | ||
const href = encode(obj.getIn(["data", "href"]) || ""); | ||
return `[${children.trim()}](${href})`; | ||
case "code-line": | ||
return `\`${children}\``; | ||
return href ? `[${children.trim()}](${href})` : children.trim(); | ||
case "mention": | ||
@@ -168,3 +161,3 @@ const username = obj.getIn(["data", "username"]) || ""; | ||
serialize(obj, children) { | ||
if (obj.kind !== "mark") return; | ||
if (obj.object !== "mark") return; | ||
switch (obj.type) { | ||
@@ -174,7 +167,7 @@ case "bold": | ||
case "italic": | ||
return `*${children}*`; | ||
return `_${children}_`; | ||
case "code": | ||
return `\`${children}\``; | ||
case "inserted": | ||
return `__${children}__`; | ||
return `++${children}++`; | ||
case "deleted": | ||
@@ -237,5 +230,14 @@ return `~~${children}~~`; | ||
serializeNode(node, document) { | ||
if (node.kind == "text") { | ||
if (node.object == "text") { | ||
const leaves = node.getLeaves(); | ||
return leaves.map(this.serializeLeaves); | ||
const inCodeBlock = !!document.getClosest( | ||
node.key, | ||
n => n.type === "code" | ||
); | ||
return leaves.map(leave => { | ||
const inCodeMark = !!leave.marks.filter(mark => mark.type === "code") | ||
.size; | ||
return this.serializeLeaves(leave, !inCodeBlock && !inCodeMark); | ||
}); | ||
} | ||
@@ -262,4 +264,11 @@ | ||
serializeLeaves(leaves) { | ||
const string = new String({ text: leaves.text }); | ||
serializeLeaves(leaves, escape = true) { | ||
let leavesText = leaves.text; | ||
if (escape) { | ||
// escape markdown characters | ||
leavesText = leavesText | ||
.replace(/([\\`*{}\[\]()#+\-.!_>]@%)/gi, "\\$1") | ||
.replace(/\n/g, " \n"); // format softBreaks | ||
} | ||
const string = new String({ text: leavesText }); | ||
const text = this.serializeString(string); | ||
@@ -266,0 +275,0 @@ |
// to ensure markdown compatability we need to specifically encode some characters | ||
export function encode(href: string) { | ||
return href | ||
export function encode(href) { | ||
return decodeSafe(href) | ||
.trim() | ||
@@ -12,3 +12,22 @@ .replace(/ /g, "%20") | ||
export function decode(href: string) { | ||
return decodeURI(href); | ||
try { | ||
return decodeURI(href); | ||
} catch (e) { | ||
return decodeSafe(href); | ||
} | ||
} | ||
// convert hanging % characters into percentage encoded %25 as decodeURI cannot | ||
// handle this scenario but users may input 'invalid' urls. | ||
function decodeSafe(uri) { | ||
const components = uri.split(/(%(?:d0|d1)%.{2})/); | ||
return components | ||
.map(component => { | ||
try { | ||
return decodeURIComponent(component); | ||
} catch (e) { | ||
return component.replace(/%(?!\d+)/g, "%25"); | ||
} | ||
}) | ||
.join(""); | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
284333
3023