gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery
This commit is contained in:
+42
@@ -0,0 +1,42 @@
|
||||
# prosemirror-markdown
|
||||
|
||||
[ [**WEBSITE**](http://prosemirror.net) | [**ISSUES**](https://github.com/prosemirror/prosemirror-markdown/issues) | [**FORUM**](https://discuss.prosemirror.net) | [**GITTER**](https://gitter.im/ProseMirror/prosemirror) ]
|
||||
|
||||
This is a (non-core) module for [ProseMirror](http://prosemirror.net).
|
||||
ProseMirror is a well-behaved rich semantic content editor based on
|
||||
contentEditable, with support for collaborative editing and custom
|
||||
document schemas.
|
||||
|
||||
This module implements a ProseMirror
|
||||
[schema](https://prosemirror.net/docs/guide/#schema) that corresponds to
|
||||
the document schema used by [CommonMark](http://commonmark.org/), and
|
||||
a parser and serializer to convert between ProseMirror documents in
|
||||
that schema and CommonMark/Markdown text.
|
||||
|
||||
This code is released under an
|
||||
[MIT license](https://github.com/prosemirror/prosemirror/tree/master/LICENSE).
|
||||
There's a [forum](http://discuss.prosemirror.net) for general
|
||||
discussion and support requests, and the
|
||||
[Github bug tracker](https://github.com/prosemirror/prosemirror/issues)
|
||||
is the place to report issues.
|
||||
|
||||
We aim to be an inclusive, welcoming community. To make that explicit,
|
||||
we have a [code of
|
||||
conduct](http://contributor-covenant.org/version/1/1/0/) that applies
|
||||
to communication around the project.
|
||||
|
||||
## Documentation
|
||||
|
||||
@schema
|
||||
|
||||
@MarkdownParser
|
||||
|
||||
@ParseSpec
|
||||
|
||||
@defaultMarkdownParser
|
||||
|
||||
@MarkdownSerializer
|
||||
|
||||
@MarkdownSerializerState
|
||||
|
||||
@defaultMarkdownSerializer
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
// @ts-ignore
|
||||
import MarkdownIt from "markdown-it"
|
||||
import Token from "markdown-it/lib/token.mjs"
|
||||
import {schema} from "./schema"
|
||||
import {Mark, MarkType, Node, Attrs, Schema, NodeType} from "prosemirror-model"
|
||||
|
||||
function maybeMerge(a: Node, b: Node): Node | undefined {
|
||||
if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks))
|
||||
return (a as any).withText(a.text! + b.text!)
|
||||
}
|
||||
|
||||
// Object used to track the context of a running parse.
|
||||
class MarkdownParseState {
|
||||
stack: {type: NodeType, attrs: Attrs | null, content: Node[], marks: readonly Mark[]}[]
|
||||
|
||||
constructor(
|
||||
readonly schema: Schema,
|
||||
readonly tokenHandlers: {[token: string]: (stat: MarkdownParseState, token: Token, tokens: Token[], i: number) => void}
|
||||
) {
|
||||
this.stack = [{type: schema.topNodeType, attrs: null, content: [], marks: Mark.none}]
|
||||
}
|
||||
|
||||
top() {
|
||||
return this.stack[this.stack.length - 1]
|
||||
}
|
||||
|
||||
push(elt: Node) {
|
||||
if (this.stack.length) this.top().content.push(elt)
|
||||
}
|
||||
|
||||
// Adds the given text to the current position in the document,
|
||||
// using the current marks as styling.
|
||||
addText(text: string) {
|
||||
if (!text) return
|
||||
let top = this.top(), nodes = top.content, last = nodes[nodes.length - 1]
|
||||
let node = this.schema.text(text, top.marks), merged
|
||||
if (last && (merged = maybeMerge(last, node))) nodes[nodes.length - 1] = merged
|
||||
else nodes.push(node)
|
||||
}
|
||||
|
||||
// Adds the given mark to the set of active marks.
|
||||
openMark(mark: Mark) {
|
||||
let top = this.top()
|
||||
top.marks = mark.addToSet(top.marks)
|
||||
}
|
||||
|
||||
// Removes the given mark from the set of active marks.
|
||||
closeMark(mark: MarkType) {
|
||||
let top = this.top()
|
||||
top.marks = mark.removeFromSet(top.marks)
|
||||
}
|
||||
|
||||
parseTokens(toks: Token[]) {
|
||||
for (let i = 0; i < toks.length; i++) {
|
||||
let tok = toks[i]
|
||||
let handler = this.tokenHandlers[tok.type]
|
||||
if (!handler)
|
||||
throw new Error("Token type `" + tok.type + "` not supported by Markdown parser")
|
||||
handler(this, tok, toks, i)
|
||||
}
|
||||
}
|
||||
|
||||
// Add a node at the current position.
|
||||
addNode(type: NodeType, attrs: Attrs | null, content?: readonly Node[]) {
|
||||
let top = this.top()
|
||||
let node = type.createAndFill(attrs, content, top ? top.marks : [])
|
||||
if (!node) return null
|
||||
this.push(node)
|
||||
return node
|
||||
}
|
||||
|
||||
// Wrap subsequent content in a node of the given type.
|
||||
openNode(type: NodeType, attrs: Attrs | null) {
|
||||
this.stack.push({type: type, attrs: attrs, content: [], marks: Mark.none})
|
||||
}
|
||||
|
||||
// Close and return the node that is currently on top of the stack.
|
||||
closeNode() {
|
||||
let info = this.stack.pop()!
|
||||
return this.addNode(info.type, info.attrs, info.content)
|
||||
}
|
||||
}
|
||||
|
||||
function attrs(spec: ParseSpec, token: Token, tokens: Token[], i: number) {
|
||||
if (spec.getAttrs) return spec.getAttrs(token, tokens, i)
|
||||
// For backwards compatibility when `attrs` is a Function
|
||||
else if (spec.attrs instanceof Function) return spec.attrs(token)
|
||||
else return spec.attrs
|
||||
}
|
||||
|
||||
// Code content is represented as a single token with a `content`
|
||||
// property in Markdown-it.
|
||||
function noCloseToken(spec: ParseSpec, type: string) {
|
||||
return spec.noCloseToken || type == "code_inline" || type == "code_block" || type == "fence"
|
||||
}
|
||||
|
||||
function withoutTrailingNewline(str: string) {
|
||||
return str[str.length - 1] == "\n" ? str.slice(0, str.length - 1) : str
|
||||
}
|
||||
|
||||
function noOp() {}
|
||||
|
||||
function tokenHandlers(schema: Schema, tokens: {[token: string]: ParseSpec}) {
|
||||
let handlers: {[token: string]: (stat: MarkdownParseState, token: Token, tokens: Token[], i: number) => void} =
|
||||
Object.create(null)
|
||||
for (let type in tokens) {
|
||||
let spec = tokens[type]
|
||||
if (spec.block) {
|
||||
let nodeType = schema.nodeType(spec.block)
|
||||
if (noCloseToken(spec, type)) {
|
||||
handlers[type] = (state, tok, tokens, i) => {
|
||||
state.openNode(nodeType, attrs(spec, tok, tokens, i))
|
||||
state.addText(withoutTrailingNewline(tok.content))
|
||||
state.closeNode()
|
||||
}
|
||||
} else {
|
||||
handlers[type + "_open"] = (state, tok, tokens, i) => state.openNode(nodeType, attrs(spec, tok, tokens, i))
|
||||
handlers[type + "_close"] = state => state.closeNode()
|
||||
}
|
||||
} else if (spec.node) {
|
||||
let nodeType = schema.nodeType(spec.node)
|
||||
handlers[type] = (state, tok, tokens, i) => state.addNode(nodeType, attrs(spec, tok, tokens, i))
|
||||
} else if (spec.mark) {
|
||||
let markType = schema.marks[spec.mark]
|
||||
if (noCloseToken(spec, type)) {
|
||||
handlers[type] = (state, tok, tokens, i) => {
|
||||
state.openMark(markType.create(attrs(spec, tok, tokens, i)))
|
||||
state.addText(withoutTrailingNewline(tok.content))
|
||||
state.closeMark(markType)
|
||||
}
|
||||
} else {
|
||||
handlers[type + "_open"] = (state, tok, tokens, i) => state.openMark(markType.create(attrs(spec, tok, tokens, i)))
|
||||
handlers[type + "_close"] = state => state.closeMark(markType)
|
||||
}
|
||||
} else if (spec.ignore) {
|
||||
if (noCloseToken(spec, type)) {
|
||||
handlers[type] = noOp
|
||||
} else {
|
||||
handlers[type + "_open"] = noOp
|
||||
handlers[type + "_close"] = noOp
|
||||
}
|
||||
} else {
|
||||
throw new RangeError("Unrecognized parsing spec " + JSON.stringify(spec))
|
||||
}
|
||||
}
|
||||
|
||||
handlers.text = (state, tok) => state.addText(tok.content)
|
||||
handlers.inline = (state, tok) => state.parseTokens(tok.children!)
|
||||
handlers.softbreak = handlers.softbreak || (state => state.addText(" "))
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
/// Object type used to specify how Markdown tokens should be parsed.
|
||||
export interface ParseSpec {
|
||||
/// This token maps to a single node, whose type can be looked up
|
||||
/// in the schema under the given name. Exactly one of `node`,
|
||||
/// `block`, or `mark` must be set.
|
||||
node?: string
|
||||
|
||||
/// This token (unless `noCloseToken` is true) comes in `_open`
|
||||
/// and `_close` variants (which are appended to the base token
|
||||
/// name provides a the object property), and wraps a block of
|
||||
/// content. The block should be wrapped in a node of the type
|
||||
/// named to by the property's value. If the token does not have
|
||||
/// `_open` or `_close`, use the `noCloseToken` option.
|
||||
block?: string
|
||||
|
||||
/// This token (again, unless `noCloseToken` is true) also comes
|
||||
/// in `_open` and `_close` variants, but should add a mark
|
||||
/// (named by the value) to its content, rather than wrapping it
|
||||
/// in a node.
|
||||
mark?: string
|
||||
|
||||
/// Attributes for the node or mark. When `getAttrs` is provided,
|
||||
/// it takes precedence.
|
||||
attrs?: Attrs | null
|
||||
|
||||
/// A function used to compute the attributes for the node or mark
|
||||
/// that takes a [markdown-it
|
||||
/// token](https://markdown-it.github.io/markdown-it/#Token) and
|
||||
/// returns an attribute object.
|
||||
getAttrs?: (token: Token, tokenStream: Token[], index: number) => Attrs | null
|
||||
|
||||
/// Indicates that the [markdown-it
|
||||
/// token](https://markdown-it.github.io/markdown-it/#Token) has
|
||||
/// no `_open` or `_close` for the nodes. This defaults to `true`
|
||||
/// for `code_inline`, `code_block` and `fence`.
|
||||
noCloseToken?: boolean
|
||||
|
||||
/// When true, ignore content for the matched token.
|
||||
ignore?: boolean
|
||||
}
|
||||
|
||||
/// A configuration of a Markdown parser. Such a parser uses
|
||||
/// [markdown-it](https://github.com/markdown-it/markdown-it) to
|
||||
/// tokenize a file, and then runs the custom rules it is given over
|
||||
/// the tokens to create a ProseMirror document tree.
|
||||
export class MarkdownParser {
|
||||
/// @internal
|
||||
tokenHandlers: {[token: string]: (stat: MarkdownParseState, token: Token, tokens: Token[], i: number) => void}
|
||||
|
||||
/// Create a parser with the given configuration. You can configure
|
||||
/// the markdown-it parser to parse the dialect you want, and provide
|
||||
/// a description of the ProseMirror entities those tokens map to in
|
||||
/// the `tokens` object, which maps token names to descriptions of
|
||||
/// what to do with them. Such a description is an object, and may
|
||||
/// have the following properties:
|
||||
constructor(
|
||||
/// The parser's document schema.
|
||||
readonly schema: Schema,
|
||||
/// This parser's markdown-it tokenizer.
|
||||
readonly tokenizer: MarkdownIt,
|
||||
/// The value of the `tokens` object used to construct this
|
||||
/// parser. Can be useful to copy and modify to base other parsers
|
||||
/// on.
|
||||
readonly tokens: {[name: string]: ParseSpec}
|
||||
) {
|
||||
this.tokenHandlers = tokenHandlers(schema, tokens)
|
||||
}
|
||||
|
||||
/// Parse a string as [CommonMark](http://commonmark.org/) markup,
|
||||
/// and create a ProseMirror document as prescribed by this parser's
|
||||
/// rules.
|
||||
///
|
||||
/// The second argument, when given, is passed through to the
|
||||
/// [Markdown
|
||||
/// parser](https://markdown-it.github.io/markdown-it/#MarkdownIt.parse).
|
||||
parse(text: string, markdownEnv: Object = {}) {
|
||||
let state = new MarkdownParseState(this.schema, this.tokenHandlers), doc
|
||||
state.parseTokens(this.tokenizer.parse(text, markdownEnv))
|
||||
do { doc = state.closeNode() } while (state.stack.length)
|
||||
return doc || this.schema.topNodeType.createAndFill()!
|
||||
}
|
||||
}
|
||||
|
||||
function listIsTight(tokens: readonly Token[], i: number) {
|
||||
while (++i < tokens.length)
|
||||
if (tokens[i].type != "list_item_open") return tokens[i].hidden
|
||||
return false
|
||||
}
|
||||
|
||||
/// A parser parsing unextended [CommonMark](http://commonmark.org/),
|
||||
/// without inline HTML, and producing a document in the basic schema.
|
||||
export const defaultMarkdownParser = new MarkdownParser(schema, MarkdownIt("commonmark", {html: false}), {
|
||||
blockquote: {block: "blockquote"},
|
||||
paragraph: {block: "paragraph"},
|
||||
list_item: {block: "list_item"},
|
||||
bullet_list: {block: "bullet_list", getAttrs: (_, tokens, i) => ({tight: listIsTight(tokens, i)})},
|
||||
ordered_list: {block: "ordered_list", getAttrs: (tok, tokens, i) => ({
|
||||
order: +tok.attrGet("start")! || 1,
|
||||
tight: listIsTight(tokens, i)
|
||||
})},
|
||||
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![0] && 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}
|
||||
})
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Defines a parser and serializer for [CommonMark](http://commonmark.org/) text.
|
||||
|
||||
export {schema} from "./schema"
|
||||
export {defaultMarkdownParser, MarkdownParser, ParseSpec} from "./from_markdown"
|
||||
export {MarkdownSerializer, defaultMarkdownSerializer, MarkdownSerializerState} from "./to_markdown"
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
import {Schema, MarkSpec} from "prosemirror-model"
|
||||
|
||||
/// Document schema for the data model used by CommonMark.
|
||||
export const schema = new Schema({
|
||||
nodes: {
|
||||
doc: {
|
||||
content: "block+"
|
||||
},
|
||||
|
||||
paragraph: {
|
||||
content: "inline*",
|
||||
group: "block",
|
||||
parseDOM: [{tag: "p"}],
|
||||
toDOM() { return ["p", 0] }
|
||||
},
|
||||
|
||||
blockquote: {
|
||||
content: "block+",
|
||||
group: "block",
|
||||
parseDOM: [{tag: "blockquote"}],
|
||||
toDOM() { return ["blockquote", 0] }
|
||||
},
|
||||
|
||||
horizontal_rule: {
|
||||
group: "block",
|
||||
parseDOM: [{tag: "hr"}],
|
||||
toDOM() { return ["div", ["hr"]] }
|
||||
},
|
||||
|
||||
heading: {
|
||||
attrs: {level: {default: 1}},
|
||||
content: "(text | image)*",
|
||||
group: "block",
|
||||
defining: true,
|
||||
parseDOM: [{tag: "h1", attrs: {level: 1}},
|
||||
{tag: "h2", attrs: {level: 2}},
|
||||
{tag: "h3", attrs: {level: 3}},
|
||||
{tag: "h4", attrs: {level: 4}},
|
||||
{tag: "h5", attrs: {level: 5}},
|
||||
{tag: "h6", attrs: {level: 6}}],
|
||||
toDOM(node) { return ["h" + node.attrs.level, 0] }
|
||||
},
|
||||
|
||||
code_block: {
|
||||
content: "text*",
|
||||
group: "block",
|
||||
code: true,
|
||||
defining: true,
|
||||
marks: "",
|
||||
attrs: {params: {default: ""}},
|
||||
parseDOM: [{tag: "pre", preserveWhitespace: "full", getAttrs: node => (
|
||||
{params: (node as HTMLElement).getAttribute("data-params") || ""}
|
||||
)}],
|
||||
toDOM(node) { return ["pre", node.attrs.params ? {"data-params": node.attrs.params} : {}, ["code", 0]] }
|
||||
},
|
||||
|
||||
ordered_list: {
|
||||
content: "list_item+",
|
||||
group: "block",
|
||||
attrs: {order: {default: 1}, tight: {default: false}},
|
||||
parseDOM: [{tag: "ol", getAttrs(dom) {
|
||||
return {order: (dom as HTMLElement).hasAttribute("start") ? +(dom as HTMLElement).getAttribute("start")! : 1,
|
||||
tight: (dom as HTMLElement).hasAttribute("data-tight")}
|
||||
}}],
|
||||
toDOM(node) {
|
||||
return ["ol", {start: node.attrs.order == 1 ? null : node.attrs.order,
|
||||
"data-tight": node.attrs.tight ? "true" : null}, 0]
|
||||
}
|
||||
},
|
||||
|
||||
bullet_list: {
|
||||
content: "list_item+",
|
||||
group: "block",
|
||||
attrs: {tight: {default: false}},
|
||||
parseDOM: [{tag: "ul", getAttrs: dom => ({tight: (dom as HTMLElement).hasAttribute("data-tight")})}],
|
||||
toDOM(node) { return ["ul", {"data-tight": node.attrs.tight ? "true" : null}, 0] }
|
||||
},
|
||||
|
||||
list_item: {
|
||||
content: "block+",
|
||||
defining: true,
|
||||
parseDOM: [{tag: "li"}],
|
||||
toDOM() { return ["li", 0] }
|
||||
},
|
||||
|
||||
text: {
|
||||
group: "inline"
|
||||
},
|
||||
|
||||
image: {
|
||||
inline: true,
|
||||
attrs: {
|
||||
src: {},
|
||||
alt: {default: null},
|
||||
title: {default: null}
|
||||
},
|
||||
group: "inline",
|
||||
draggable: true,
|
||||
parseDOM: [{tag: "img[src]", getAttrs(dom) {
|
||||
return {
|
||||
src: (dom as HTMLElement).getAttribute("src"),
|
||||
title: (dom as HTMLElement).getAttribute("title"),
|
||||
alt: (dom as HTMLElement).getAttribute("alt")
|
||||
}
|
||||
}}],
|
||||
toDOM(node) { return ["img", node.attrs] }
|
||||
},
|
||||
|
||||
hard_break: {
|
||||
inline: true,
|
||||
group: "inline",
|
||||
selectable: false,
|
||||
parseDOM: [{tag: "br"}],
|
||||
toDOM() { return ["br"] }
|
||||
}
|
||||
},
|
||||
|
||||
marks: {
|
||||
em: {
|
||||
parseDOM: [
|
||||
{tag: "i"}, {tag: "em"},
|
||||
{style: "font-style=italic"},
|
||||
{style: "font-style=normal", clearMark: m => m.type.name == "em"}
|
||||
],
|
||||
toDOM() { return ["em"] }
|
||||
},
|
||||
|
||||
strong: {
|
||||
parseDOM: [
|
||||
{tag: "strong"},
|
||||
{tag: "b", getAttrs: node => node.style.fontWeight != "normal" && null},
|
||||
{style: "font-weight=400", clearMark: m => m.type.name == "strong"},
|
||||
{style: "font-weight", getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null}
|
||||
],
|
||||
toDOM() { return ["strong"] }
|
||||
} as MarkSpec,
|
||||
|
||||
link: {
|
||||
attrs: {
|
||||
href: {},
|
||||
title: {default: null}
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [{tag: "a[href]", getAttrs(dom) {
|
||||
return {href: (dom as HTMLElement).getAttribute("href"), title: dom.getAttribute("title")}
|
||||
}}],
|
||||
toDOM(node) { return ["a", node.attrs] }
|
||||
},
|
||||
|
||||
code: {
|
||||
code: true,
|
||||
parseDOM: [{tag: "code"}],
|
||||
toDOM() { return ["code"] }
|
||||
}
|
||||
}
|
||||
})
|
||||
+483
@@ -0,0 +1,483 @@
|
||||
import {Node, Mark} from "prosemirror-model"
|
||||
|
||||
type MarkSerializerSpec = {
|
||||
/// The string that should appear before a piece of content marked
|
||||
/// by this mark, either directly or as a function that returns an
|
||||
/// appropriate string.
|
||||
open: string | ((state: MarkdownSerializerState, mark: Mark, parent: Node, index: number) => string),
|
||||
/// The string that should appear after a piece of content marked by
|
||||
/// this mark.
|
||||
close: string | ((state: MarkdownSerializerState, mark: Mark, parent: Node, index: number) => string),
|
||||
/// When `true`, this indicates that the order in which the mark's
|
||||
/// opening and closing syntax appears relative to other mixable
|
||||
/// marks can be varied. (For example, you can say `**a *b***` and
|
||||
/// `*a **b***`, but not `` `a *b*` ``.)
|
||||
mixable?: boolean,
|
||||
/// When enabled, causes the serializer to move enclosing whitespace
|
||||
/// from inside the marks to outside the marks. This is necessary
|
||||
/// for emphasis marks as CommonMark does not permit enclosing
|
||||
/// whitespace inside emphasis marks, see:
|
||||
/// http:///spec.commonmark.org/0.26/#example-330
|
||||
expelEnclosingWhitespace?: boolean,
|
||||
/// Can be set to `false` to disable character escaping in a mark. A
|
||||
/// non-escaping mark has to have the highest precedence (must
|
||||
/// always be the innermost mark).
|
||||
escape?: boolean
|
||||
}
|
||||
|
||||
const blankMark: MarkSerializerSpec = {open: "", close: "", mixable: true}
|
||||
|
||||
/// A specification for serializing a ProseMirror document as
|
||||
/// Markdown/CommonMark text.
|
||||
export class MarkdownSerializer {
|
||||
/// Construct a serializer with the given configuration. The `nodes`
|
||||
/// object should map node names in a given schema to function that
|
||||
/// take a serializer state and such a node, and serialize the node.
|
||||
constructor(
|
||||
/// The node serializer functions for this serializer.
|
||||
readonly nodes: {[node: string]: (state: MarkdownSerializerState, node: Node, parent: Node, index: number) => void},
|
||||
/// The mark serializer info.
|
||||
readonly marks: {[mark: string]: MarkSerializerSpec},
|
||||
readonly options: {
|
||||
/// Extra characters can be added for escaping. This is passed
|
||||
/// directly to String.replace(), and the matching characters are
|
||||
/// preceded by a backslash.
|
||||
escapeExtraCharacters?: RegExp,
|
||||
/// Specify the node name of hard breaks.
|
||||
/// Defaults to "hard_break"
|
||||
hardBreakNodeName?: string,
|
||||
/// By default, the serializer raises an error when it finds a
|
||||
/// node or mark type for which no serializer is defined. Set
|
||||
/// this to `false` to make it just ignore such elements,
|
||||
/// rendering only their content.
|
||||
strict?: boolean
|
||||
} = {}
|
||||
) {}
|
||||
|
||||
/// Serialize the content of the given node to
|
||||
/// [CommonMark](http://commonmark.org/).
|
||||
serialize(content: Node, options: {
|
||||
/// Whether to render lists in a tight style. This can be overridden
|
||||
/// on a node level by specifying a tight attribute on the node.
|
||||
/// Defaults to false.
|
||||
tightLists?: boolean
|
||||
} = {}) {
|
||||
options = Object.assign({}, this.options, options)
|
||||
let state = new MarkdownSerializerState(this.nodes, this.marks, options)
|
||||
state.renderContent(content)
|
||||
return state.out
|
||||
}
|
||||
}
|
||||
|
||||
/// A serializer for the [basic schema](#schema).
|
||||
export const defaultMarkdownSerializer = new MarkdownSerializer({
|
||||
blockquote(state, node) {
|
||||
state.wrapBlock("> ", null, node, () => state.renderContent(node))
|
||||
},
|
||||
code_block(state, node) {
|
||||
// Make sure the front matter fences are longer than any dash sequence within it
|
||||
const backticks = node.textContent.match(/`{3,}/gm)
|
||||
const fence = backticks ? (backticks.sort().slice(-1)[0] + "`") : "```"
|
||||
|
||||
state.write(fence + (node.attrs.params || "") + "\n")
|
||||
state.text(node.textContent, false)
|
||||
// Add a newline to the current content before adding closing marker
|
||||
state.write("\n")
|
||||
state.write(fence)
|
||||
state.closeBlock(node)
|
||||
},
|
||||
heading(state, node) {
|
||||
state.write(state.repeat("#", node.attrs.level) + " ")
|
||||
state.renderInline(node, false)
|
||||
state.closeBlock(node)
|
||||
},
|
||||
horizontal_rule(state, node) {
|
||||
state.write(node.attrs.markup || "---")
|
||||
state.closeBlock(node)
|
||||
},
|
||||
bullet_list(state, node) {
|
||||
state.renderList(node, " ", () => (node.attrs.bullet || "*") + " ")
|
||||
},
|
||||
ordered_list(state, node) {
|
||||
let start = node.attrs.order || 1
|
||||
let maxW = String(start + node.childCount - 1).length
|
||||
let space = state.repeat(" ", maxW + 2)
|
||||
state.renderList(node, space, i => {
|
||||
let 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("]/g, "\\$&") +
|
||||
(node.attrs.title ? ' "' + node.attrs.title.replace(/"/g, '\\"') + '"' : "") + ")")
|
||||
},
|
||||
hard_break(state, node, parent, index) {
|
||||
for (let i = index + 1; i < parent.childCount; i++)
|
||||
if (parent.child(i).type != node.type) {
|
||||
state.write("\\\n")
|
||||
return
|
||||
}
|
||||
},
|
||||
text(state, node) {
|
||||
state.text(node.text!, !state.inAutolink)
|
||||
}
|
||||
}, {
|
||||
em: {open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true},
|
||||
strong: {open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true},
|
||||
link: {
|
||||
open(state, mark, parent, index) {
|
||||
state.inAutolink = isPlainURL(mark, parent, index)
|
||||
return state.inAutolink ? "<" : "["
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
let {inAutolink} = state
|
||||
state.inAutolink = undefined
|
||||
return inAutolink ? ">"
|
||||
: "](" + mark.attrs.href.replace(/[\(\)"]/g, "\\$&") + (mark.attrs.title ? ` "${mark.attrs.title.replace(/"/g, '\\"')}"` : "") + ")"
|
||||
},
|
||||
mixable: true
|
||||
},
|
||||
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}
|
||||
})
|
||||
|
||||
function backticksFor(node: Node, side: number) {
|
||||
let ticks = /`+/g, m: RegExpExecArray | null, 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: Mark, parent: Node, index: number) {
|
||||
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false
|
||||
let content = parent.child(index)
|
||||
if (!content.isText || content.text != link.attrs.href || content.marks[content.marks.length - 1] != link) return false
|
||||
return index == parent.childCount - 1 || !link.isInSet(parent.child(index + 1).marks)
|
||||
}
|
||||
|
||||
/// This is an object used to track state and expose
|
||||
/// methods related to markdown serialization. Instances are passed to
|
||||
/// node and mark serialization methods (see `toMarkdown`).
|
||||
export class MarkdownSerializerState {
|
||||
/// @internal
|
||||
delim: string = ""
|
||||
/// @internal
|
||||
out: string = ""
|
||||
/// @internal
|
||||
closed: Node | null = null
|
||||
/// @internal
|
||||
inAutolink: boolean | undefined = undefined
|
||||
/// @internal
|
||||
atBlockStart: boolean = false
|
||||
/// @internal
|
||||
inTightList: boolean = false
|
||||
|
||||
/// @internal
|
||||
constructor(
|
||||
/// @internal
|
||||
readonly nodes: {[node: string]: (state: MarkdownSerializerState, node: Node, parent: Node, index: number) => void},
|
||||
/// @internal
|
||||
readonly marks: {[mark: string]: MarkSerializerSpec},
|
||||
/// The options passed to the serializer.
|
||||
readonly options: {tightLists?: boolean, escapeExtraCharacters?: RegExp, hardBreakNodeName?: string, strict?: boolean}
|
||||
) {
|
||||
if (typeof this.options.tightLists == "undefined")
|
||||
this.options.tightLists = false
|
||||
if (typeof this.options.hardBreakNodeName == "undefined")
|
||||
this.options.hardBreakNodeName = "hard_break"
|
||||
}
|
||||
|
||||
/// @internal
|
||||
flushClose(size: number = 2) {
|
||||
if (this.closed) {
|
||||
if (!this.atBlank()) this.out += "\n"
|
||||
if (size > 1) {
|
||||
let delimMin = this.delim
|
||||
let trim = /\s+$/.exec(delimMin)
|
||||
if (trim) delimMin = delimMin.slice(0, delimMin.length - trim[0].length)
|
||||
for (let i = 1; i < size; i++)
|
||||
this.out += delimMin + "\n"
|
||||
}
|
||||
this.closed = null
|
||||
}
|
||||
}
|
||||
|
||||
/// @internal
|
||||
getMark(name: string) {
|
||||
let info = this.marks[name]
|
||||
if (!info) {
|
||||
if (this.options.strict !== false)
|
||||
throw new Error(`Mark type \`${name}\` not supported by Markdown renderer`)
|
||||
info = blankMark
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
/// Render a block, prefixing each line with `delim`, and the first
|
||||
/// line in `firstDelim`. `node` should be the node that is closed at
|
||||
/// the end of the block, and `f` is a function that renders the
|
||||
/// content of the block.
|
||||
wrapBlock(delim: string, firstDelim: string | null, node: Node, f: () => void) {
|
||||
let old = this.delim
|
||||
this.write(firstDelim != null ? firstDelim : delim)
|
||||
this.delim += delim
|
||||
f()
|
||||
this.delim = old
|
||||
this.closeBlock(node)
|
||||
}
|
||||
|
||||
/// @internal
|
||||
atBlank() {
|
||||
return /(^|\n)$/.test(this.out)
|
||||
}
|
||||
|
||||
/// Ensure the current content ends with a newline.
|
||||
ensureNewLine() {
|
||||
if (!this.atBlank()) this.out += "\n"
|
||||
}
|
||||
|
||||
/// Prepare the state for writing output (closing closed paragraphs,
|
||||
/// adding delimiters, and so on), and then optionally add content
|
||||
/// (unescaped) to the output.
|
||||
write(content?: string) {
|
||||
this.flushClose()
|
||||
if (this.delim && this.atBlank())
|
||||
this.out += this.delim
|
||||
if (content) this.out += content
|
||||
}
|
||||
|
||||
/// Close the block for the given node.
|
||||
closeBlock(node: Node) {
|
||||
this.closed = node
|
||||
}
|
||||
|
||||
/// Add the given text to the document. When escape is not `false`,
|
||||
/// it will be escaped.
|
||||
text(text: string, escape = true) {
|
||||
let lines = text.split("\n")
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
this.write()
|
||||
// Escape exclamation marks in front of links
|
||||
if (!escape && lines[i][0] == "[" && /(^|[^\\])\!$/.test(this.out))
|
||||
this.out = this.out.slice(0, this.out.length - 1) + "\\!"
|
||||
this.out += escape ? this.esc(lines[i], this.atBlockStart) : lines[i]
|
||||
if (i != lines.length - 1) this.out += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the given node as a block.
|
||||
render(node: Node, parent: Node, index: number) {
|
||||
if (this.nodes[node.type.name]) {
|
||||
this.nodes[node.type.name](this, node, parent, index)
|
||||
} else {
|
||||
if (this.options.strict !== false) {
|
||||
throw new Error("Token type `" + node.type.name + "` not supported by Markdown renderer")
|
||||
} else if (!node.type.isLeaf) {
|
||||
if (node.type.inlineContent) this.renderInline(node)
|
||||
else this.renderContent(node)
|
||||
if (node.isBlock) this.closeBlock(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the contents of `parent` as block nodes.
|
||||
renderContent(parent: Node) {
|
||||
parent.forEach((node, _, i) => this.render(node, parent, i))
|
||||
}
|
||||
|
||||
/// Render the contents of `parent` as inline content.
|
||||
renderInline(parent: Node, fromBlockStart = true) {
|
||||
this.atBlockStart = fromBlockStart
|
||||
let active: Mark[] = [], trailing = ""
|
||||
let progress = (node: Node | null, offset: number, index: number) => {
|
||||
let marks = node ? node.marks : []
|
||||
|
||||
// Remove marks from `hard_break` that are the last node inside
|
||||
// that mark to prevent parser edge cases with new lines just
|
||||
// before closing marks.
|
||||
if (node && node.type.name === this.options.hardBreakNodeName)
|
||||
marks = marks.filter(m => {
|
||||
if (index + 1 == parent.childCount) return false
|
||||
let next = parent.child(index + 1)
|
||||
return m.isInSet(next.marks) && (!next.isText || /\S/.test(next.text!))
|
||||
})
|
||||
|
||||
let leading = trailing
|
||||
trailing = ""
|
||||
// If whitespace has to be expelled from the node, adjust
|
||||
// leading and trailing accordingly.
|
||||
if (node && node.isText && marks.some(mark => {
|
||||
let info = this.getMark(mark.type.name)
|
||||
return info && info.expelEnclosingWhitespace && !mark.isInSet(active)
|
||||
})) {
|
||||
let [_, lead, rest] = /^(\s*)(.*)$/m.exec(node.text!)!
|
||||
if (lead) {
|
||||
leading += lead
|
||||
node = rest ? (node as any).withText(rest) : null
|
||||
if (!node) marks = active
|
||||
}
|
||||
}
|
||||
if (node && node.isText && marks.some(mark => {
|
||||
let info = this.getMark(mark.type.name)
|
||||
return info && info.expelEnclosingWhitespace && !this.isMarkAhead(parent, index + 1, mark)
|
||||
})) {
|
||||
let [_, rest, trail] = /^(.*?)(\s*)$/m.exec(node.text!)!
|
||||
if (trail) {
|
||||
trailing = trail
|
||||
node = rest ? (node as any).withText(rest) : null
|
||||
if (!node) marks = active
|
||||
}
|
||||
}
|
||||
let inner = marks.length ? marks[marks.length - 1] : null
|
||||
let noEsc = inner && this.getMark(inner.type.name).escape === false
|
||||
let len = marks.length - (noEsc ? 1 : 0)
|
||||
|
||||
// Try to reorder 'mixable' marks, such as em and strong, which
|
||||
// in Markdown may be opened and closed in different order, so
|
||||
// that order of the marks for the token matches the order in
|
||||
// active.
|
||||
outer: for (let i = 0; i < len; i++) {
|
||||
let mark = marks[i]
|
||||
if (!this.getMark(mark.type.name).mixable) break
|
||||
for (let j = 0; j < active.length; j++) {
|
||||
let other = active[j]
|
||||
if (!this.getMark(other.type.name).mixable) break
|
||||
if (mark.eq(other)) {
|
||||
if (i > j)
|
||||
marks = marks.slice(0, j).concat(mark).concat(marks.slice(j, i)).concat(marks.slice(i + 1, len))
|
||||
else if (j > i)
|
||||
marks = marks.slice(0, i).concat(marks.slice(i + 1, j)).concat(mark).concat(marks.slice(j, len))
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the prefix of the mark set that didn't change
|
||||
let keep = 0
|
||||
while (keep < Math.min(active.length, len) && marks[keep].eq(active[keep])) ++keep
|
||||
|
||||
// Close the marks that need to be closed
|
||||
while (keep < active.length)
|
||||
this.text(this.markString(active.pop()!, false, parent, index), false)
|
||||
|
||||
// Output any previously expelled trailing whitespace outside the marks
|
||||
if (leading) this.text(leading)
|
||||
|
||||
// Open the marks that need to be opened
|
||||
if (node) {
|
||||
while (active.length < len) {
|
||||
let add = marks[active.length]
|
||||
active.push(add)
|
||||
this.text(this.markString(add, true, parent, index), false)
|
||||
this.atBlockStart = false
|
||||
}
|
||||
|
||||
// Render the node. Special case code marks, since their content
|
||||
// may not be escaped.
|
||||
if (noEsc && node.isText)
|
||||
this.text(this.markString(inner!, true, parent, index) + node.text +
|
||||
this.markString(inner!, false, parent, index + 1), false)
|
||||
else
|
||||
this.render(node, parent, index)
|
||||
this.atBlockStart = false
|
||||
}
|
||||
|
||||
// After the first non-empty text node is rendered, the end of output
|
||||
// is no longer at block start.
|
||||
//
|
||||
// FIXME: If a non-text node writes something to the output for this
|
||||
// block, the end of output is also no longer at block start. But how
|
||||
// can we detect that?
|
||||
if (node?.isText && node.nodeSize > 0) {
|
||||
this.atBlockStart = false
|
||||
}
|
||||
}
|
||||
parent.forEach(progress)
|
||||
progress(null, 0, parent.childCount)
|
||||
this.atBlockStart = false
|
||||
}
|
||||
|
||||
/// Render a node's content as a list. `delim` should be the extra
|
||||
/// indentation added to all lines except the first in an item,
|
||||
/// `firstDelim` is a function going from an item index to a
|
||||
/// delimiter for the first line of the item.
|
||||
renderList(node: Node, delim: string, firstDelim: (index: number) => string) {
|
||||
if (this.closed && this.closed.type == node.type)
|
||||
this.flushClose(3)
|
||||
else if (this.inTightList)
|
||||
this.flushClose(1)
|
||||
|
||||
let isTight = typeof node.attrs.tight != "undefined" ? node.attrs.tight : this.options.tightLists
|
||||
let prevTight = this.inTightList
|
||||
this.inTightList = isTight
|
||||
node.forEach((child, _, i) => {
|
||||
if (i && isTight) this.flushClose(1)
|
||||
this.wrapBlock(delim, firstDelim(i), node, () => this.render(child, node, i))
|
||||
})
|
||||
this.inTightList = prevTight
|
||||
}
|
||||
|
||||
/// Escape the given string so that it can safely appear in Markdown
|
||||
/// content. If `startOfLine` is true, also escape characters that
|
||||
/// have special meaning only at the start of the line.
|
||||
esc(str: string, startOfLine = false) {
|
||||
str = str.replace(
|
||||
/[`*\\~\[\]_]/g,
|
||||
(m, i) => m == "_" && i > 0 && i + 1 < str.length && str[i-1].match(/\w/) && str[i+1].match(/\w/) ? m : "\\" + m
|
||||
)
|
||||
if (startOfLine) str = str.replace(/^(\+[ ]|[\-*>])/, "\\$&").replace(/^(\s*)(#{1,6})(\s|$)/, '$1\\$2$3').replace(/^(\s*\d+)\.\s/, "$1\\. ")
|
||||
if (this.options.escapeExtraCharacters) str = str.replace(this.options.escapeExtraCharacters, "\\$&")
|
||||
return str
|
||||
}
|
||||
|
||||
/// @internal
|
||||
quote(str: string) {
|
||||
let wrap = str.indexOf('"') == -1 ? '""' : str.indexOf("'") == -1 ? "''" : "()"
|
||||
return wrap[0] + str + wrap[1]
|
||||
}
|
||||
|
||||
/// Repeat the given string `n` times.
|
||||
repeat(str: string, n: number) {
|
||||
let out = ""
|
||||
for (let i = 0; i < n; i++) out += str
|
||||
return out
|
||||
}
|
||||
|
||||
/// Get the markdown string for a given opening or closing mark.
|
||||
markString(mark: Mark, open: boolean, parent: Node, index: number) {
|
||||
let info = this.getMark(mark.type.name)
|
||||
let value = open ? info.open : info.close
|
||||
return typeof value == "string" ? value : value(this, mark, parent, index)
|
||||
}
|
||||
|
||||
/// Get leading and trailing whitespace from a string. Values of
|
||||
/// leading or trailing property of the return object will be undefined
|
||||
/// if there is no match.
|
||||
getEnclosingWhitespace(text: string): {leading?: string, trailing?: string} {
|
||||
return {
|
||||
leading: (text.match(/^(\s+)/) || [undefined])[0],
|
||||
trailing: (text.match(/(\s+)$/) || [undefined])[0]
|
||||
}
|
||||
}
|
||||
|
||||
/// @internal
|
||||
isMarkAhead(parent: Node, index: number, mark: Mark) {
|
||||
for (;; index++) {
|
||||
if (index >= parent.childCount) return false
|
||||
let next = parent.child(index)
|
||||
if (next.type.name != this.options.hardBreakNodeName) return mark.isInSet(next.marks)
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user