fix: resolve TypeScript errors in frontend build

This commit is contained in:
Hiro
2026-03-30 23:16:07 +00:00
parent b733306773
commit 24925e1acb
2941 changed files with 418042 additions and 49 deletions

42
node_modules/prosemirror-markdown/src/README.md generated vendored Normal file
View File

@@ -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
node_modules/prosemirror-markdown/src/from_markdown.ts generated vendored Normal file
View File

@@ -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
node_modules/prosemirror-markdown/src/index.ts generated vendored Normal file
View File

@@ -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
node_modules/prosemirror-markdown/src/schema.ts generated vendored Normal file
View File

@@ -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
node_modules/prosemirror-markdown/src/to_markdown.ts generated vendored Normal file
View File

@@ -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("![" + state.esc(node.attrs.alt || "") + "](" + node.attrs.src.replace(/[\(\)]/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++
}
}
}