diff --git a/dist/ast-BXYwy08e.d.cts b/dist/ast-DbjiuYr8.d.cts similarity index 78% rename from dist/ast-BXYwy08e.d.cts rename to dist/ast-DbjiuYr8.d.cts index bc5dc9e..bc89b83 100644 --- a/dist/ast-BXYwy08e.d.cts +++ b/dist/ast-DbjiuYr8.d.cts @@ -6,7 +6,7 @@ interface MarkdownPosition { readonly endColumn: number; } type MarkdownNode = MarkdownBlockNode | MarkdownInlineNode; -type MarkdownBlockNode = MarkdownDocumentNode | MarkdownParagraphNode | MarkdownHeadingNode | MarkdownBlockquoteNode | MarkdownListNode | MarkdownListItemNode | MarkdownCodeBlockNode | MarkdownThematicBreakNode | MarkdownHtmlBlockNode | MarkdownTableNode | MarkdownTableRowNode | MarkdownTableCellNode; +type MarkdownBlockNode = MarkdownDocumentNode | MarkdownParagraphNode | MarkdownHeadingNode | MarkdownBlockquoteNode | MarkdownListNode | MarkdownListItemNode | MarkdownCodeBlockNode | MarkdownThematicBreakNode | MarkdownHtmlBlockNode | MarkdownTableNode | MarkdownTableRowNode | MarkdownTableCellNode | MarkdownMathBlockNode; interface MarkdownDocumentNode { readonly type: 'document'; readonly children: MarkdownBlockNode[]; @@ -84,7 +84,12 @@ interface MarkdownTableCellNode { readonly children: MarkdownInlineNode[]; readonly position?: MarkdownPosition; } -type MarkdownInlineNode = MarkdownTextNode | MarkdownEmphasisNode | MarkdownStrongNode | MarkdownStrikethroughNode | MarkdownCodeSpanNode | MarkdownLinkNode | MarkdownImageNode | MarkdownAutolinkNode | MarkdownHardBreakNode | MarkdownSoftBreakNode | MarkdownRawHtmlNode | MarkdownEntityNode; +interface MarkdownMathBlockNode { + readonly type: 'mathBlock'; + readonly literal: string; + readonly position?: MarkdownPosition; +} +type MarkdownInlineNode = MarkdownTextNode | MarkdownEmphasisNode | MarkdownStrongNode | MarkdownStrikethroughNode | MarkdownCodeSpanNode | MarkdownLinkNode | MarkdownImageNode | MarkdownAutolinkNode | MarkdownHardBreakNode | MarkdownSoftBreakNode | MarkdownRawHtmlNode | MarkdownEntityNode | MarkdownMathInlineNode; type MarkdownEmphasisMarker = '_' | '*'; interface MarkdownTextNode { readonly type: 'text'; @@ -154,7 +159,12 @@ interface MarkdownEntityNode { readonly value: string; readonly position?: MarkdownPosition; } +interface MarkdownMathInlineNode { + readonly type: 'mathInline'; + readonly literal: string; + readonly position?: MarkdownPosition; +} declare function isMarkdownBlockNode(node: MarkdownNode): node is MarkdownBlockNode; declare function isMarkdownInlineNode(node: MarkdownNode): node is MarkdownInlineNode; //#endregion -export { MarkdownTableCellNode as A, MarkdownParagraphNode as C, MarkdownStrikethroughNode as D, MarkdownSoftBreakNode as E, isMarkdownBlockNode as F, isMarkdownInlineNode as I, MarkdownTableRowNode as M, MarkdownTextNode as N, MarkdownStrongNode as O, MarkdownThematicBreakNode as P, MarkdownOrderedListDelimiter as S, MarkdownRawHtmlNode as T, MarkdownLinkNode as _, MarkdownCodeBlockNode as a, MarkdownListNode as b, MarkdownEmphasisMarker as c, MarkdownHardBreakNode as d, MarkdownHeadingNode as f, MarkdownInlineNode as g, MarkdownImageNode as h, MarkdownBulletMarker as i, MarkdownTableNode as j, MarkdownTableAlignment as k, MarkdownEmphasisNode as l, MarkdownHtmlBlockNode as m, MarkdownBlockNode as n, MarkdownCodeSpanNode as o, MarkdownHeadingStyle as p, MarkdownBlockquoteNode as r, MarkdownDocumentNode as s, MarkdownAutolinkNode as t, MarkdownEntityNode as u, MarkdownListItemNode as v, MarkdownPosition as w, MarkdownNode as x, MarkdownListMarkerType as y }; \ No newline at end of file +export { MarkdownStrongNode as A, MarkdownNode as C, MarkdownRawHtmlNode as D, MarkdownPosition as E, MarkdownTextNode as F, MarkdownThematicBreakNode as I, isMarkdownBlockNode as L, MarkdownTableCellNode as M, MarkdownTableNode as N, MarkdownSoftBreakNode as O, MarkdownTableRowNode as P, isMarkdownInlineNode as R, MarkdownMathInlineNode as S, MarkdownParagraphNode as T, MarkdownLinkNode as _, MarkdownCodeBlockNode as a, MarkdownListNode as b, MarkdownEmphasisMarker as c, MarkdownHardBreakNode as d, MarkdownHeadingNode as f, MarkdownInlineNode as g, MarkdownImageNode as h, MarkdownBulletMarker as i, MarkdownTableAlignment as j, MarkdownStrikethroughNode as k, MarkdownEmphasisNode as l, MarkdownHtmlBlockNode as m, MarkdownBlockNode as n, MarkdownCodeSpanNode as o, MarkdownHeadingStyle as p, MarkdownBlockquoteNode as r, MarkdownDocumentNode as s, MarkdownAutolinkNode as t, MarkdownEntityNode as u, MarkdownListItemNode as v, MarkdownOrderedListDelimiter as w, MarkdownMathBlockNode as x, MarkdownListMarkerType as y }; \ No newline at end of file diff --git a/dist/ast-BXYwy08e.d.ts b/dist/ast-DbjiuYr8.d.ts similarity index 78% rename from dist/ast-BXYwy08e.d.ts rename to dist/ast-DbjiuYr8.d.ts index bc5dc9e..bc89b83 100644 --- a/dist/ast-BXYwy08e.d.ts +++ b/dist/ast-DbjiuYr8.d.ts @@ -6,7 +6,7 @@ interface MarkdownPosition { readonly endColumn: number; } type MarkdownNode = MarkdownBlockNode | MarkdownInlineNode; -type MarkdownBlockNode = MarkdownDocumentNode | MarkdownParagraphNode | MarkdownHeadingNode | MarkdownBlockquoteNode | MarkdownListNode | MarkdownListItemNode | MarkdownCodeBlockNode | MarkdownThematicBreakNode | MarkdownHtmlBlockNode | MarkdownTableNode | MarkdownTableRowNode | MarkdownTableCellNode; +type MarkdownBlockNode = MarkdownDocumentNode | MarkdownParagraphNode | MarkdownHeadingNode | MarkdownBlockquoteNode | MarkdownListNode | MarkdownListItemNode | MarkdownCodeBlockNode | MarkdownThematicBreakNode | MarkdownHtmlBlockNode | MarkdownTableNode | MarkdownTableRowNode | MarkdownTableCellNode | MarkdownMathBlockNode; interface MarkdownDocumentNode { readonly type: 'document'; readonly children: MarkdownBlockNode[]; @@ -84,7 +84,12 @@ interface MarkdownTableCellNode { readonly children: MarkdownInlineNode[]; readonly position?: MarkdownPosition; } -type MarkdownInlineNode = MarkdownTextNode | MarkdownEmphasisNode | MarkdownStrongNode | MarkdownStrikethroughNode | MarkdownCodeSpanNode | MarkdownLinkNode | MarkdownImageNode | MarkdownAutolinkNode | MarkdownHardBreakNode | MarkdownSoftBreakNode | MarkdownRawHtmlNode | MarkdownEntityNode; +interface MarkdownMathBlockNode { + readonly type: 'mathBlock'; + readonly literal: string; + readonly position?: MarkdownPosition; +} +type MarkdownInlineNode = MarkdownTextNode | MarkdownEmphasisNode | MarkdownStrongNode | MarkdownStrikethroughNode | MarkdownCodeSpanNode | MarkdownLinkNode | MarkdownImageNode | MarkdownAutolinkNode | MarkdownHardBreakNode | MarkdownSoftBreakNode | MarkdownRawHtmlNode | MarkdownEntityNode | MarkdownMathInlineNode; type MarkdownEmphasisMarker = '_' | '*'; interface MarkdownTextNode { readonly type: 'text'; @@ -154,7 +159,12 @@ interface MarkdownEntityNode { readonly value: string; readonly position?: MarkdownPosition; } +interface MarkdownMathInlineNode { + readonly type: 'mathInline'; + readonly literal: string; + readonly position?: MarkdownPosition; +} declare function isMarkdownBlockNode(node: MarkdownNode): node is MarkdownBlockNode; declare function isMarkdownInlineNode(node: MarkdownNode): node is MarkdownInlineNode; //#endregion -export { MarkdownTableCellNode as A, MarkdownParagraphNode as C, MarkdownStrikethroughNode as D, MarkdownSoftBreakNode as E, isMarkdownBlockNode as F, isMarkdownInlineNode as I, MarkdownTableRowNode as M, MarkdownTextNode as N, MarkdownStrongNode as O, MarkdownThematicBreakNode as P, MarkdownOrderedListDelimiter as S, MarkdownRawHtmlNode as T, MarkdownLinkNode as _, MarkdownCodeBlockNode as a, MarkdownListNode as b, MarkdownEmphasisMarker as c, MarkdownHardBreakNode as d, MarkdownHeadingNode as f, MarkdownInlineNode as g, MarkdownImageNode as h, MarkdownBulletMarker as i, MarkdownTableNode as j, MarkdownTableAlignment as k, MarkdownEmphasisNode as l, MarkdownHtmlBlockNode as m, MarkdownBlockNode as n, MarkdownCodeSpanNode as o, MarkdownHeadingStyle as p, MarkdownBlockquoteNode as r, MarkdownDocumentNode as s, MarkdownAutolinkNode as t, MarkdownEntityNode as u, MarkdownListItemNode as v, MarkdownPosition as w, MarkdownNode as x, MarkdownListMarkerType as y }; \ No newline at end of file +export { MarkdownStrongNode as A, MarkdownNode as C, MarkdownRawHtmlNode as D, MarkdownPosition as E, MarkdownTextNode as F, MarkdownThematicBreakNode as I, isMarkdownBlockNode as L, MarkdownTableCellNode as M, MarkdownTableNode as N, MarkdownSoftBreakNode as O, MarkdownTableRowNode as P, isMarkdownInlineNode as R, MarkdownMathInlineNode as S, MarkdownParagraphNode as T, MarkdownLinkNode as _, MarkdownCodeBlockNode as a, MarkdownListNode as b, MarkdownEmphasisMarker as c, MarkdownHardBreakNode as d, MarkdownHeadingNode as f, MarkdownInlineNode as g, MarkdownImageNode as h, MarkdownBulletMarker as i, MarkdownTableAlignment as j, MarkdownStrikethroughNode as k, MarkdownEmphasisNode as l, MarkdownHtmlBlockNode as m, MarkdownBlockNode as n, MarkdownCodeSpanNode as o, MarkdownHeadingStyle as p, MarkdownBlockquoteNode as r, MarkdownDocumentNode as s, MarkdownAutolinkNode as t, MarkdownEntityNode as u, MarkdownListItemNode as v, MarkdownOrderedListDelimiter as w, MarkdownMathBlockNode as x, MarkdownListMarkerType as y }; \ No newline at end of file diff --git a/dist/ast/ast.cjs b/dist/ast/ast.cjs index 113e784..181bcec 100644 --- a/dist/ast/ast.cjs +++ b/dist/ast/ast.cjs @@ -12,7 +12,8 @@ const BLOCK_NODE_TYPES = /* @__PURE__ */ new Set([ "htmlBlock", "table", "tableRow", - "tableCell" + "tableCell", + "mathBlock" ]); function isMarkdownBlockNode(node) { return BLOCK_NODE_TYPES.has(node.type); diff --git a/dist/ast/ast.d.cts b/dist/ast/ast.d.cts index 70c2936..f9f760e 100644 --- a/dist/ast/ast.d.cts +++ b/dist/ast/ast.d.cts @@ -1,2 +1,2 @@ -import { A as MarkdownTableCellNode, C as MarkdownParagraphNode, D as MarkdownStrikethroughNode, E as MarkdownSoftBreakNode, F as isMarkdownBlockNode, I as isMarkdownInlineNode, M as MarkdownTableRowNode, N as MarkdownTextNode, O as MarkdownStrongNode, P as MarkdownThematicBreakNode, S as MarkdownOrderedListDelimiter, T as MarkdownRawHtmlNode, _ as MarkdownLinkNode, a as MarkdownCodeBlockNode, b as MarkdownListNode, c as MarkdownEmphasisMarker, d as MarkdownHardBreakNode, f as MarkdownHeadingNode, g as MarkdownInlineNode, h as MarkdownImageNode, i as MarkdownBulletMarker, j as MarkdownTableNode, k as MarkdownTableAlignment, l as MarkdownEmphasisNode, m as MarkdownHtmlBlockNode, n as MarkdownBlockNode, o as MarkdownCodeSpanNode, p as MarkdownHeadingStyle, r as MarkdownBlockquoteNode, s as MarkdownDocumentNode, t as MarkdownAutolinkNode, u as MarkdownEntityNode, v as MarkdownListItemNode, w as MarkdownPosition, x as MarkdownNode, y as MarkdownListMarkerType } from "../ast-BXYwy08e.cjs"; -export { MarkdownAutolinkNode, MarkdownBlockNode, MarkdownBlockquoteNode, MarkdownBulletMarker, MarkdownCodeBlockNode, MarkdownCodeSpanNode, MarkdownDocumentNode, MarkdownEmphasisMarker, MarkdownEmphasisNode, MarkdownEntityNode, MarkdownHardBreakNode, MarkdownHeadingNode, MarkdownHeadingStyle, MarkdownHtmlBlockNode, MarkdownImageNode, MarkdownInlineNode, MarkdownLinkNode, MarkdownListItemNode, MarkdownListMarkerType, MarkdownListNode, MarkdownNode, MarkdownOrderedListDelimiter, MarkdownParagraphNode, MarkdownPosition, MarkdownRawHtmlNode, MarkdownSoftBreakNode, MarkdownStrikethroughNode, MarkdownStrongNode, MarkdownTableAlignment, MarkdownTableCellNode, MarkdownTableNode, MarkdownTableRowNode, MarkdownTextNode, MarkdownThematicBreakNode, isMarkdownBlockNode, isMarkdownInlineNode }; \ No newline at end of file +import { A as MarkdownStrongNode, C as MarkdownNode, D as MarkdownRawHtmlNode, E as MarkdownPosition, F as MarkdownTextNode, I as MarkdownThematicBreakNode, L as isMarkdownBlockNode, M as MarkdownTableCellNode, N as MarkdownTableNode, O as MarkdownSoftBreakNode, P as MarkdownTableRowNode, R as isMarkdownInlineNode, S as MarkdownMathInlineNode, T as MarkdownParagraphNode, _ as MarkdownLinkNode, a as MarkdownCodeBlockNode, b as MarkdownListNode, c as MarkdownEmphasisMarker, d as MarkdownHardBreakNode, f as MarkdownHeadingNode, g as MarkdownInlineNode, h as MarkdownImageNode, i as MarkdownBulletMarker, j as MarkdownTableAlignment, k as MarkdownStrikethroughNode, l as MarkdownEmphasisNode, m as MarkdownHtmlBlockNode, n as MarkdownBlockNode, o as MarkdownCodeSpanNode, p as MarkdownHeadingStyle, r as MarkdownBlockquoteNode, s as MarkdownDocumentNode, t as MarkdownAutolinkNode, u as MarkdownEntityNode, v as MarkdownListItemNode, w as MarkdownOrderedListDelimiter, x as MarkdownMathBlockNode, y as MarkdownListMarkerType } from "../ast-DbjiuYr8.cjs"; +export { MarkdownAutolinkNode, MarkdownBlockNode, MarkdownBlockquoteNode, MarkdownBulletMarker, MarkdownCodeBlockNode, MarkdownCodeSpanNode, MarkdownDocumentNode, MarkdownEmphasisMarker, MarkdownEmphasisNode, MarkdownEntityNode, MarkdownHardBreakNode, MarkdownHeadingNode, MarkdownHeadingStyle, MarkdownHtmlBlockNode, MarkdownImageNode, MarkdownInlineNode, MarkdownLinkNode, MarkdownListItemNode, MarkdownListMarkerType, MarkdownListNode, MarkdownMathBlockNode, MarkdownMathInlineNode, MarkdownNode, MarkdownOrderedListDelimiter, MarkdownParagraphNode, MarkdownPosition, MarkdownRawHtmlNode, MarkdownSoftBreakNode, MarkdownStrikethroughNode, MarkdownStrongNode, MarkdownTableAlignment, MarkdownTableCellNode, MarkdownTableNode, MarkdownTableRowNode, MarkdownTextNode, MarkdownThematicBreakNode, isMarkdownBlockNode, isMarkdownInlineNode }; \ No newline at end of file diff --git a/dist/ast/ast.d.ts b/dist/ast/ast.d.ts index d37d46d..931f9bc 100644 --- a/dist/ast/ast.d.ts +++ b/dist/ast/ast.d.ts @@ -1,2 +1,2 @@ -import { A as MarkdownTableCellNode, C as MarkdownParagraphNode, D as MarkdownStrikethroughNode, E as MarkdownSoftBreakNode, F as isMarkdownBlockNode, I as isMarkdownInlineNode, M as MarkdownTableRowNode, N as MarkdownTextNode, O as MarkdownStrongNode, P as MarkdownThematicBreakNode, S as MarkdownOrderedListDelimiter, T as MarkdownRawHtmlNode, _ as MarkdownLinkNode, a as MarkdownCodeBlockNode, b as MarkdownListNode, c as MarkdownEmphasisMarker, d as MarkdownHardBreakNode, f as MarkdownHeadingNode, g as MarkdownInlineNode, h as MarkdownImageNode, i as MarkdownBulletMarker, j as MarkdownTableNode, k as MarkdownTableAlignment, l as MarkdownEmphasisNode, m as MarkdownHtmlBlockNode, n as MarkdownBlockNode, o as MarkdownCodeSpanNode, p as MarkdownHeadingStyle, r as MarkdownBlockquoteNode, s as MarkdownDocumentNode, t as MarkdownAutolinkNode, u as MarkdownEntityNode, v as MarkdownListItemNode, w as MarkdownPosition, x as MarkdownNode, y as MarkdownListMarkerType } from "../ast-BXYwy08e.js"; -export { MarkdownAutolinkNode, MarkdownBlockNode, MarkdownBlockquoteNode, MarkdownBulletMarker, MarkdownCodeBlockNode, MarkdownCodeSpanNode, MarkdownDocumentNode, MarkdownEmphasisMarker, MarkdownEmphasisNode, MarkdownEntityNode, MarkdownHardBreakNode, MarkdownHeadingNode, MarkdownHeadingStyle, MarkdownHtmlBlockNode, MarkdownImageNode, MarkdownInlineNode, MarkdownLinkNode, MarkdownListItemNode, MarkdownListMarkerType, MarkdownListNode, MarkdownNode, MarkdownOrderedListDelimiter, MarkdownParagraphNode, MarkdownPosition, MarkdownRawHtmlNode, MarkdownSoftBreakNode, MarkdownStrikethroughNode, MarkdownStrongNode, MarkdownTableAlignment, MarkdownTableCellNode, MarkdownTableNode, MarkdownTableRowNode, MarkdownTextNode, MarkdownThematicBreakNode, isMarkdownBlockNode, isMarkdownInlineNode }; \ No newline at end of file +import { A as MarkdownStrongNode, C as MarkdownNode, D as MarkdownRawHtmlNode, E as MarkdownPosition, F as MarkdownTextNode, I as MarkdownThematicBreakNode, L as isMarkdownBlockNode, M as MarkdownTableCellNode, N as MarkdownTableNode, O as MarkdownSoftBreakNode, P as MarkdownTableRowNode, R as isMarkdownInlineNode, S as MarkdownMathInlineNode, T as MarkdownParagraphNode, _ as MarkdownLinkNode, a as MarkdownCodeBlockNode, b as MarkdownListNode, c as MarkdownEmphasisMarker, d as MarkdownHardBreakNode, f as MarkdownHeadingNode, g as MarkdownInlineNode, h as MarkdownImageNode, i as MarkdownBulletMarker, j as MarkdownTableAlignment, k as MarkdownStrikethroughNode, l as MarkdownEmphasisNode, m as MarkdownHtmlBlockNode, n as MarkdownBlockNode, o as MarkdownCodeSpanNode, p as MarkdownHeadingStyle, r as MarkdownBlockquoteNode, s as MarkdownDocumentNode, t as MarkdownAutolinkNode, u as MarkdownEntityNode, v as MarkdownListItemNode, w as MarkdownOrderedListDelimiter, x as MarkdownMathBlockNode, y as MarkdownListMarkerType } from "../ast-DbjiuYr8.js"; +export { MarkdownAutolinkNode, MarkdownBlockNode, MarkdownBlockquoteNode, MarkdownBulletMarker, MarkdownCodeBlockNode, MarkdownCodeSpanNode, MarkdownDocumentNode, MarkdownEmphasisMarker, MarkdownEmphasisNode, MarkdownEntityNode, MarkdownHardBreakNode, MarkdownHeadingNode, MarkdownHeadingStyle, MarkdownHtmlBlockNode, MarkdownImageNode, MarkdownInlineNode, MarkdownLinkNode, MarkdownListItemNode, MarkdownListMarkerType, MarkdownListNode, MarkdownMathBlockNode, MarkdownMathInlineNode, MarkdownNode, MarkdownOrderedListDelimiter, MarkdownParagraphNode, MarkdownPosition, MarkdownRawHtmlNode, MarkdownSoftBreakNode, MarkdownStrikethroughNode, MarkdownStrongNode, MarkdownTableAlignment, MarkdownTableCellNode, MarkdownTableNode, MarkdownTableRowNode, MarkdownTextNode, MarkdownThematicBreakNode, isMarkdownBlockNode, isMarkdownInlineNode }; \ No newline at end of file diff --git a/dist/ast/ast.js b/dist/ast/ast.js index df8682b..8b29c9b 100644 --- a/dist/ast/ast.js +++ b/dist/ast/ast.js @@ -11,7 +11,8 @@ const BLOCK_NODE_TYPES = /* @__PURE__ */ new Set([ "htmlBlock", "table", "tableRow", - "tableCell" + "tableCell", + "mathBlock" ]); function isMarkdownBlockNode(node) { return BLOCK_NODE_TYPES.has(node.type); diff --git a/dist/block/block.cjs b/dist/block/block.cjs index 28c2136..eb483d9 100644 --- a/dist/block/block.cjs +++ b/dist/block/block.cjs @@ -14,12 +14,14 @@ const TASK_LIST_MARKER_PATTERN = /^\[([ xX])\][ \t]/; const NUL_REPLACEMENT = "�"; const NUL_PATTERN = /\0/g; const LINE_ENDING_PATTERN = /\r\n|\n|\r/; -const MAYBE_SPECIAL_PATTERN = /^[#`~*+_=<>0-9|:-]/; +const MAYBE_SPECIAL_PATTERN = /^[#$`~*+_=<>0-9|:-]/; const ATX_MARKER_PATTERN = /^#{1,6}(?:[ \t]+|$)/; const ATX_ONLY_CLOSING_SEQUENCE_PATTERN = /^[ \t]*#+[ \t]*$/; const ATX_TRAILING_CLOSING_SEQUENCE_PATTERN = /[ \t]+#+[ \t]*$/; const CODE_FENCE_PATTERN = /^`{3,}(?!.*`)|^~{3,}/; const CLOSING_CODE_FENCE_PATTERN = /^(?:`{3,}|~{3,})(?=[ \t]*$)/; +const MATH_BLOCK_MARKER_PATTERN = /^\$\$[ \t]*$/; +const MATH_BLOCK_MARKER_LENGTH = 2; const SETEXT_UNDERLINE_PATTERN = /^(?:=+|-+)[ \t]*$/; const THEMATIC_BREAK_PATTERN = /^(?:\*[ \t]*){3,}$|^(?:_[ \t]*){3,}$|^(?:-[ \t]*){3,}$/; const BLANK_CONTENT_PATTERN = /^[ \t\n]*$/; @@ -83,10 +85,19 @@ var BlockParser = class { }); return; } - if (node.kind === "htmlBlock" && !HTML_BLOCK_BLANK_LINE_END_TYPES.includes(node.htmlBlockType)) this.sink({ - code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.UNTERMINATED_HTML_BLOCK, + if (node.kind === "htmlBlock" && !HTML_BLOCK_BLANK_LINE_END_TYPES.includes(node.htmlBlockType)) { + this.sink({ + code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.UNTERMINATED_HTML_BLOCK, + severity: "warning", + message: `HTML block (type ${String(node.htmlBlockType)}) starting at line ${String(node.startLine)} never met its own end condition before the end of the document`, + line: node.startLine + }); + return; + } + if (node.kind === "mathBlock") this.sink({ + code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.UNCLOSED_MATH_BLOCK, severity: "warning", - message: `HTML block (type ${String(node.htmlBlockType)}) starting at line ${String(node.startLine)} never met its own end condition before the end of the document`, + message: `math block starting at line ${String(node.startLine)} was never closed by a matching closing $$ before the end of the document`, line: node.startLine }); } @@ -119,6 +130,7 @@ var BlockParser = class { case "blockquote": return this.continueBlockquote(); case "listItem": return this.continueListItem(node); case "codeBlock": return this.continueCodeBlock(node); + case "mathBlock": return this.continueMathBlock(node); case "htmlBlock": return this.line.blank && HTML_BLOCK_BLANK_LINE_END_TYPES.includes(node.htmlBlockType) ? "not-matched" : "matched"; case "paragraph": case "table": return this.line.blank ? "not-matched" : "matched"; @@ -172,6 +184,13 @@ var BlockParser = class { for (let remaining = node.fenceOffset; remaining > 0 && this.line.peek() === " "; remaining -= 1) this.line.advance(1); return "matched"; } + continueMathBlock(node) { + if (!this.line.indented && MATH_BLOCK_MARKER_PATTERN.test(this.line.restFromNextNonspace())) { + this.finalize(node); + return "finished"; + } + return "matched"; + } openNewBlocks(matchedContainer) { let container = matchedContainer; let matchedLeaf = container.kind !== "paragraph" && container.kind !== "table" && require_block_node.acceptsLines(container.kind); @@ -196,6 +215,7 @@ var BlockParser = class { () => this.tryBlockquoteStart(), () => this.tryAtxHeadingStart(), () => this.tryCodeFenceStart(), + () => this.tryMathBlockStart(), () => this.tryHtmlBlockStart(container), () => this.tryPromoteParagraph(container), () => this.tryThematicBreakStart(), @@ -244,6 +264,15 @@ var BlockParser = class { this.line.advance(fence.length); return "leaf"; } + tryMathBlockStart() { + if (this.line.indented) return "none"; + if (!MATH_BLOCK_MARKER_PATTERN.test(this.line.restFromNextNonspace())) return "none"; + this.closeUnmatchedBlocks(); + this.addChild("mathBlock"); + this.line.advanceToNextNonspace(); + this.line.advance(MATH_BLOCK_MARKER_LENGTH); + return "leaf"; + } tryHtmlBlockStart(container) { if (this.line.indented || this.line.peekNextNonspace() !== "<") return "none"; const interruptsParagraph = container.kind === "paragraph" || !this.allClosed && !this.line.blank && this.tip.kind === "paragraph"; @@ -374,6 +403,9 @@ var BlockParser = class { case "codeBlock": this.finalizeCodeBlock(node); return; + case "mathBlock": + this.finalizeMathBlock(node); + return; case "htmlBlock": node.literal = node.content.replace(TRAILING_HTML_BLANK_LINES_PATTERN, ""); return; @@ -392,6 +424,10 @@ var BlockParser = class { node.infoString = require_inline_entity.unescapeString(node.content.slice(0, breakIndex).trim()); node.literal = node.content.slice(breakIndex + 1); } + finalizeMathBlock(node) { + const breakIndex = node.content.indexOf("\n"); + node.literal = breakIndex === -1 ? "" : node.content.slice(breakIndex + 1); + } }; function toInlineChildren(content, references, options) { return require_inline_inline.parseInlines(content.trim(), references, options); @@ -501,6 +537,10 @@ function toAstBlock(node, references, options) { literal: node.literal }; case "thematicBreak": return { type: "thematicBreak" }; + case "mathBlock": return { + type: "mathBlock", + literal: node.literal + }; case "table": return toTableNode(node, references, options); case "document": case "listItem": return; diff --git a/dist/block/block.d.cts b/dist/block/block.d.cts index a76ed9f..6061c80 100644 --- a/dist/block/block.d.cts +++ b/dist/block/block.d.cts @@ -1,7 +1,7 @@ -import { s as MarkdownDocumentNode } from "../ast-BXYwy08e.cjs"; -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.cjs"; +import { s as MarkdownDocumentNode } from "../ast-DbjiuYr8.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.cjs"; import { n as LinkReferenceMap } from "../link-Dv4kxVjk.cjs"; -import { t as InlineParseOptions } from "../inline-C9YEGDV5.cjs"; +import { t as InlineParseOptions } from "../inline-uVHJ5xzT.cjs"; //#region src/block/block.d.ts interface MarkdownParseOptions extends InlineParseOptions { readonly gfmTables?: boolean; diff --git a/dist/block/block.d.ts b/dist/block/block.d.ts index da8085c..e4d752a 100644 --- a/dist/block/block.d.ts +++ b/dist/block/block.d.ts @@ -1,7 +1,7 @@ -import { s as MarkdownDocumentNode } from "../ast-BXYwy08e.js"; -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.js"; +import { s as MarkdownDocumentNode } from "../ast-DbjiuYr8.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.js"; import { n as LinkReferenceMap } from "../link-Dv4kxVjk.js"; -import { t as InlineParseOptions } from "../inline-B50cM6cn.js"; +import { t as InlineParseOptions } from "../inline-TuBQ2TUr.js"; //#region src/block/block.d.ts interface MarkdownParseOptions extends InlineParseOptions { readonly gfmTables?: boolean; diff --git a/dist/block/block.js b/dist/block/block.js index 529d5c8..6064516 100644 --- a/dist/block/block.js +++ b/dist/block/block.js @@ -13,12 +13,14 @@ const TASK_LIST_MARKER_PATTERN = /^\[([ xX])\][ \t]/; const NUL_REPLACEMENT = "�"; const NUL_PATTERN = /\0/g; const LINE_ENDING_PATTERN = /\r\n|\n|\r/; -const MAYBE_SPECIAL_PATTERN = /^[#`~*+_=<>0-9|:-]/; +const MAYBE_SPECIAL_PATTERN = /^[#$`~*+_=<>0-9|:-]/; const ATX_MARKER_PATTERN = /^#{1,6}(?:[ \t]+|$)/; const ATX_ONLY_CLOSING_SEQUENCE_PATTERN = /^[ \t]*#+[ \t]*$/; const ATX_TRAILING_CLOSING_SEQUENCE_PATTERN = /[ \t]+#+[ \t]*$/; const CODE_FENCE_PATTERN = /^`{3,}(?!.*`)|^~{3,}/; const CLOSING_CODE_FENCE_PATTERN = /^(?:`{3,}|~{3,})(?=[ \t]*$)/; +const MATH_BLOCK_MARKER_PATTERN = /^\$\$[ \t]*$/; +const MATH_BLOCK_MARKER_LENGTH = 2; const SETEXT_UNDERLINE_PATTERN = /^(?:=+|-+)[ \t]*$/; const THEMATIC_BREAK_PATTERN = /^(?:\*[ \t]*){3,}$|^(?:_[ \t]*){3,}$|^(?:-[ \t]*){3,}$/; const BLANK_CONTENT_PATTERN = /^[ \t\n]*$/; @@ -82,10 +84,19 @@ var BlockParser = class { }); return; } - if (node.kind === "htmlBlock" && !HTML_BLOCK_BLANK_LINE_END_TYPES.includes(node.htmlBlockType)) this.sink({ - code: MarkdownDiagnosticCodes.UNTERMINATED_HTML_BLOCK, + if (node.kind === "htmlBlock" && !HTML_BLOCK_BLANK_LINE_END_TYPES.includes(node.htmlBlockType)) { + this.sink({ + code: MarkdownDiagnosticCodes.UNTERMINATED_HTML_BLOCK, + severity: "warning", + message: `HTML block (type ${String(node.htmlBlockType)}) starting at line ${String(node.startLine)} never met its own end condition before the end of the document`, + line: node.startLine + }); + return; + } + if (node.kind === "mathBlock") this.sink({ + code: MarkdownDiagnosticCodes.UNCLOSED_MATH_BLOCK, severity: "warning", - message: `HTML block (type ${String(node.htmlBlockType)}) starting at line ${String(node.startLine)} never met its own end condition before the end of the document`, + message: `math block starting at line ${String(node.startLine)} was never closed by a matching closing $$ before the end of the document`, line: node.startLine }); } @@ -118,6 +129,7 @@ var BlockParser = class { case "blockquote": return this.continueBlockquote(); case "listItem": return this.continueListItem(node); case "codeBlock": return this.continueCodeBlock(node); + case "mathBlock": return this.continueMathBlock(node); case "htmlBlock": return this.line.blank && HTML_BLOCK_BLANK_LINE_END_TYPES.includes(node.htmlBlockType) ? "not-matched" : "matched"; case "paragraph": case "table": return this.line.blank ? "not-matched" : "matched"; @@ -171,6 +183,13 @@ var BlockParser = class { for (let remaining = node.fenceOffset; remaining > 0 && this.line.peek() === " "; remaining -= 1) this.line.advance(1); return "matched"; } + continueMathBlock(node) { + if (!this.line.indented && MATH_BLOCK_MARKER_PATTERN.test(this.line.restFromNextNonspace())) { + this.finalize(node); + return "finished"; + } + return "matched"; + } openNewBlocks(matchedContainer) { let container = matchedContainer; let matchedLeaf = container.kind !== "paragraph" && container.kind !== "table" && acceptsLines(container.kind); @@ -195,6 +214,7 @@ var BlockParser = class { () => this.tryBlockquoteStart(), () => this.tryAtxHeadingStart(), () => this.tryCodeFenceStart(), + () => this.tryMathBlockStart(), () => this.tryHtmlBlockStart(container), () => this.tryPromoteParagraph(container), () => this.tryThematicBreakStart(), @@ -243,6 +263,15 @@ var BlockParser = class { this.line.advance(fence.length); return "leaf"; } + tryMathBlockStart() { + if (this.line.indented) return "none"; + if (!MATH_BLOCK_MARKER_PATTERN.test(this.line.restFromNextNonspace())) return "none"; + this.closeUnmatchedBlocks(); + this.addChild("mathBlock"); + this.line.advanceToNextNonspace(); + this.line.advance(MATH_BLOCK_MARKER_LENGTH); + return "leaf"; + } tryHtmlBlockStart(container) { if (this.line.indented || this.line.peekNextNonspace() !== "<") return "none"; const interruptsParagraph = container.kind === "paragraph" || !this.allClosed && !this.line.blank && this.tip.kind === "paragraph"; @@ -373,6 +402,9 @@ var BlockParser = class { case "codeBlock": this.finalizeCodeBlock(node); return; + case "mathBlock": + this.finalizeMathBlock(node); + return; case "htmlBlock": node.literal = node.content.replace(TRAILING_HTML_BLANK_LINES_PATTERN, ""); return; @@ -391,6 +423,10 @@ var BlockParser = class { node.infoString = unescapeString(node.content.slice(0, breakIndex).trim()); node.literal = node.content.slice(breakIndex + 1); } + finalizeMathBlock(node) { + const breakIndex = node.content.indexOf("\n"); + node.literal = breakIndex === -1 ? "" : node.content.slice(breakIndex + 1); + } }; function toInlineChildren(content, references, options) { return parseInlines(content.trim(), references, options); @@ -500,6 +536,10 @@ function toAstBlock(node, references, options) { literal: node.literal }; case "thematicBreak": return { type: "thematicBreak" }; + case "mathBlock": return { + type: "mathBlock", + literal: node.literal + }; case "table": return toTableNode(node, references, options); case "document": case "listItem": return; diff --git a/dist/block/definitions.d.cts b/dist/block/definitions.d.cts index bb134fa..a988d01 100644 --- a/dist/block/definitions.d.cts +++ b/dist/block/definitions.d.cts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.cjs"; import { t as LinkReferenceDefinition } from "../link-Dv4kxVjk.cjs"; //#region src/block/definitions.d.ts declare function extractDefinitions(content: string, references: Map, sink?: MarkdownDiagnosticSink, startLine?: number): string; diff --git a/dist/block/definitions.d.ts b/dist/block/definitions.d.ts index 8fcfa46..8b6318b 100644 --- a/dist/block/definitions.d.ts +++ b/dist/block/definitions.d.ts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.js"; import { t as LinkReferenceDefinition } from "../link-Dv4kxVjk.js"; //#region src/block/definitions.d.ts declare function extractDefinitions(content: string, references: Map, sink?: MarkdownDiagnosticSink, startLine?: number): string; diff --git a/dist/block/node.cjs b/dist/block/node.cjs index d83b5f8..bb6750e 100644 --- a/dist/block/node.cjs +++ b/dist/block/node.cjs @@ -60,7 +60,7 @@ function canContain(parent, child) { } } function acceptsLines(kind) { - return kind === "paragraph" || kind === "codeBlock" || kind === "htmlBlock" || kind === "table"; + return kind === "paragraph" || kind === "codeBlock" || kind === "htmlBlock" || kind === "table" || kind === "mathBlock"; } //#endregion exports.BlockNode = BlockNode; diff --git a/dist/block/node.d.cts b/dist/block/node.d.cts index cd7148f..75f36bb 100644 --- a/dist/block/node.d.cts +++ b/dist/block/node.d.cts @@ -1,7 +1,7 @@ -import { S as MarkdownOrderedListDelimiter, i as MarkdownBulletMarker, k as MarkdownTableAlignment } from "../ast-BXYwy08e.cjs"; +import { i as MarkdownBulletMarker, j as MarkdownTableAlignment, w as MarkdownOrderedListDelimiter } from "../ast-DbjiuYr8.cjs"; import { t as HtmlBlockType } from "../html-bkz2QTuq.cjs"; //#region src/block/node.d.ts -type BlockNodeKind = 'document' | 'paragraph' | 'heading' | 'blockquote' | 'list' | 'listItem' | 'codeBlock' | 'htmlBlock' | 'thematicBreak' | 'table'; +type BlockNodeKind = 'document' | 'paragraph' | 'heading' | 'blockquote' | 'list' | 'listItem' | 'codeBlock' | 'htmlBlock' | 'thematicBreak' | 'table' | 'mathBlock'; type BlockHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; interface ListMarkerData { readonly type: 'bullet' | 'ordered'; diff --git a/dist/block/node.d.ts b/dist/block/node.d.ts index f409cd2..2e36796 100644 --- a/dist/block/node.d.ts +++ b/dist/block/node.d.ts @@ -1,7 +1,7 @@ -import { S as MarkdownOrderedListDelimiter, i as MarkdownBulletMarker, k as MarkdownTableAlignment } from "../ast-BXYwy08e.js"; +import { i as MarkdownBulletMarker, j as MarkdownTableAlignment, w as MarkdownOrderedListDelimiter } from "../ast-DbjiuYr8.js"; import { t as HtmlBlockType } from "../html-bkz2QTuq.js"; //#region src/block/node.d.ts -type BlockNodeKind = 'document' | 'paragraph' | 'heading' | 'blockquote' | 'list' | 'listItem' | 'codeBlock' | 'htmlBlock' | 'thematicBreak' | 'table'; +type BlockNodeKind = 'document' | 'paragraph' | 'heading' | 'blockquote' | 'list' | 'listItem' | 'codeBlock' | 'htmlBlock' | 'thematicBreak' | 'table' | 'mathBlock'; type BlockHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; interface ListMarkerData { readonly type: 'bullet' | 'ordered'; diff --git a/dist/block/node.js b/dist/block/node.js index 7f18f15..0fb02b8 100644 --- a/dist/block/node.js +++ b/dist/block/node.js @@ -59,7 +59,7 @@ function canContain(parent, child) { } } function acceptsLines(kind) { - return kind === "paragraph" || kind === "codeBlock" || kind === "htmlBlock" || kind === "table"; + return kind === "paragraph" || kind === "codeBlock" || kind === "htmlBlock" || kind === "table" || kind === "mathBlock"; } //#endregion export { BlockNode, acceptsLines, canContain }; diff --git a/dist/block/table.d.cts b/dist/block/table.d.cts index c0449b3..3c9a2ed 100644 --- a/dist/block/table.d.cts +++ b/dist/block/table.d.cts @@ -1,4 +1,4 @@ -import { k as MarkdownTableAlignment } from "../ast-BXYwy08e.cjs"; +import { j as MarkdownTableAlignment } from "../ast-DbjiuYr8.cjs"; //#region src/block/table.d.ts declare function splitTableRow(line: string): string[]; declare function parseTableDelimiterRow(line: string): MarkdownTableAlignment[] | undefined; diff --git a/dist/block/table.d.ts b/dist/block/table.d.ts index 98eb368..cfbfd5d 100644 --- a/dist/block/table.d.ts +++ b/dist/block/table.d.ts @@ -1,4 +1,4 @@ -import { k as MarkdownTableAlignment } from "../ast-BXYwy08e.js"; +import { j as MarkdownTableAlignment } from "../ast-DbjiuYr8.js"; //#region src/block/table.d.ts declare function splitTableRow(line: string): string[]; declare function parseTableDelimiterRow(line: string): MarkdownTableAlignment[] | undefined; diff --git a/dist/diagnostics-DmuWc7d8.d.cts b/dist/diagnostics-B72W0P_E.d.cts similarity index 93% rename from dist/diagnostics-DmuWc7d8.d.cts rename to dist/diagnostics-B72W0P_E.d.cts index ce353ef..377d5e9 100644 --- a/dist/diagnostics-DmuWc7d8.d.cts +++ b/dist/diagnostics-B72W0P_E.d.cts @@ -10,6 +10,7 @@ type MarkdownDiagnosticSink = (diagnostic: MarkdownDiagnostic) => void; declare const NOOP_MARKDOWN_DIAGNOSTIC_SINK: MarkdownDiagnosticSink; declare const MarkdownDiagnosticCodes: { readonly UNCLOSED_FENCE: "md/unclosed-fence"; + readonly UNCLOSED_MATH_BLOCK: "md/unclosed-math-block"; readonly UNTERMINATED_HTML_BLOCK: "md/unterminated-html-block"; readonly TABLE_CELL_COUNT_MISMATCH: "md/table-cell-count-mismatch"; readonly DUPLICATE_LINK_REFERENCE: "md/duplicate-link-reference"; @@ -24,6 +25,8 @@ declare const MarkdownDiagnosticCodes: { readonly IMAGE_UNRESOLVED: "md/image-unresolved"; readonly RAW_HTML_PRESERVED_AS_TEXT: "md/raw-html-preserved-as-text"; readonly RAW_HTML_DROPPED: "md/raw-html-dropped"; + readonly MATH_BLOCK_PRESERVED_AS_TEXT: "md/math-block-preserved-as-text"; + readonly MATH_INLINE_PRESERVED_AS_TEXT: "md/math-inline-preserved-as-text"; readonly FRONT_MATTER_KEY_UNMAPPED: "md/front-matter-key-unmapped"; readonly HEADING_LEVEL_CLAMPED: "md/heading-level-clamped"; readonly ADJACENT_LINKS_MERGED: "md/adjacent-links-merged"; diff --git a/dist/diagnostics-DmuWc7d8.d.ts b/dist/diagnostics-B72W0P_E.d.ts similarity index 93% rename from dist/diagnostics-DmuWc7d8.d.ts rename to dist/diagnostics-B72W0P_E.d.ts index ce353ef..377d5e9 100644 --- a/dist/diagnostics-DmuWc7d8.d.ts +++ b/dist/diagnostics-B72W0P_E.d.ts @@ -10,6 +10,7 @@ type MarkdownDiagnosticSink = (diagnostic: MarkdownDiagnostic) => void; declare const NOOP_MARKDOWN_DIAGNOSTIC_SINK: MarkdownDiagnosticSink; declare const MarkdownDiagnosticCodes: { readonly UNCLOSED_FENCE: "md/unclosed-fence"; + readonly UNCLOSED_MATH_BLOCK: "md/unclosed-math-block"; readonly UNTERMINATED_HTML_BLOCK: "md/unterminated-html-block"; readonly TABLE_CELL_COUNT_MISMATCH: "md/table-cell-count-mismatch"; readonly DUPLICATE_LINK_REFERENCE: "md/duplicate-link-reference"; @@ -24,6 +25,8 @@ declare const MarkdownDiagnosticCodes: { readonly IMAGE_UNRESOLVED: "md/image-unresolved"; readonly RAW_HTML_PRESERVED_AS_TEXT: "md/raw-html-preserved-as-text"; readonly RAW_HTML_DROPPED: "md/raw-html-dropped"; + readonly MATH_BLOCK_PRESERVED_AS_TEXT: "md/math-block-preserved-as-text"; + readonly MATH_INLINE_PRESERVED_AS_TEXT: "md/math-inline-preserved-as-text"; readonly FRONT_MATTER_KEY_UNMAPPED: "md/front-matter-key-unmapped"; readonly HEADING_LEVEL_CLAMPED: "md/heading-level-clamped"; readonly ADJACENT_LINKS_MERGED: "md/adjacent-links-merged"; diff --git a/dist/diagnostics/diagnostics.cjs b/dist/diagnostics/diagnostics.cjs index 3b1f87f..d477ccc 100644 --- a/dist/diagnostics/diagnostics.cjs +++ b/dist/diagnostics/diagnostics.cjs @@ -3,6 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const NOOP_MARKDOWN_DIAGNOSTIC_SINK = () => {}; const MarkdownDiagnosticCodes = { UNCLOSED_FENCE: "md/unclosed-fence", + UNCLOSED_MATH_BLOCK: "md/unclosed-math-block", UNTERMINATED_HTML_BLOCK: "md/unterminated-html-block", TABLE_CELL_COUNT_MISMATCH: "md/table-cell-count-mismatch", DUPLICATE_LINK_REFERENCE: "md/duplicate-link-reference", @@ -17,6 +18,8 @@ const MarkdownDiagnosticCodes = { IMAGE_UNRESOLVED: "md/image-unresolved", RAW_HTML_PRESERVED_AS_TEXT: "md/raw-html-preserved-as-text", RAW_HTML_DROPPED: "md/raw-html-dropped", + MATH_BLOCK_PRESERVED_AS_TEXT: "md/math-block-preserved-as-text", + MATH_INLINE_PRESERVED_AS_TEXT: "md/math-inline-preserved-as-text", FRONT_MATTER_KEY_UNMAPPED: "md/front-matter-key-unmapped", HEADING_LEVEL_CLAMPED: "md/heading-level-clamped", ADJACENT_LINKS_MERGED: "md/adjacent-links-merged", diff --git a/dist/diagnostics/diagnostics.d.cts b/dist/diagnostics/diagnostics.d.cts index e21e508..8c2dd8b 100644 --- a/dist/diagnostics/diagnostics.d.cts +++ b/dist/diagnostics/diagnostics.d.cts @@ -1,2 +1,2 @@ -import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnsupportedDocumentKindError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownWriteError } from "../diagnostics-DmuWc7d8.cjs"; +import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnsupportedDocumentKindError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownWriteError } from "../diagnostics-B72W0P_E.cjs"; export { MarkdownDiagnostic, MarkdownDiagnosticCodes, MarkdownDiagnosticSeverity, MarkdownDiagnosticSink, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownParseError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK }; \ No newline at end of file diff --git a/dist/diagnostics/diagnostics.d.ts b/dist/diagnostics/diagnostics.d.ts index d300371..3b7ce16 100644 --- a/dist/diagnostics/diagnostics.d.ts +++ b/dist/diagnostics/diagnostics.d.ts @@ -1,2 +1,2 @@ -import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnsupportedDocumentKindError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownWriteError } from "../diagnostics-DmuWc7d8.js"; +import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnsupportedDocumentKindError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownWriteError } from "../diagnostics-B72W0P_E.js"; export { MarkdownDiagnostic, MarkdownDiagnosticCodes, MarkdownDiagnosticSeverity, MarkdownDiagnosticSink, MarkdownInputTooLargeError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownParseError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK }; \ No newline at end of file diff --git a/dist/diagnostics/diagnostics.js b/dist/diagnostics/diagnostics.js index 60b3060..dbcabf3 100644 --- a/dist/diagnostics/diagnostics.js +++ b/dist/diagnostics/diagnostics.js @@ -2,6 +2,7 @@ const NOOP_MARKDOWN_DIAGNOSTIC_SINK = () => {}; const MarkdownDiagnosticCodes = { UNCLOSED_FENCE: "md/unclosed-fence", + UNCLOSED_MATH_BLOCK: "md/unclosed-math-block", UNTERMINATED_HTML_BLOCK: "md/unterminated-html-block", TABLE_CELL_COUNT_MISMATCH: "md/table-cell-count-mismatch", DUPLICATE_LINK_REFERENCE: "md/duplicate-link-reference", @@ -16,6 +17,8 @@ const MarkdownDiagnosticCodes = { IMAGE_UNRESOLVED: "md/image-unresolved", RAW_HTML_PRESERVED_AS_TEXT: "md/raw-html-preserved-as-text", RAW_HTML_DROPPED: "md/raw-html-dropped", + MATH_BLOCK_PRESERVED_AS_TEXT: "md/math-block-preserved-as-text", + MATH_INLINE_PRESERVED_AS_TEXT: "md/math-inline-preserved-as-text", FRONT_MATTER_KEY_UNMAPPED: "md/front-matter-key-unmapped", HEADING_LEVEL_CLAMPED: "md/heading-level-clamped", ADJACENT_LINKS_MERGED: "md/adjacent-links-merged", diff --git a/dist/emit/emit.cjs b/dist/emit/emit.cjs index 1fea9d2..765f94e 100644 --- a/dist/emit/emit.cjs +++ b/dist/emit/emit.cjs @@ -34,7 +34,8 @@ const QUOTABLE_STYLE_IDS = /* @__PURE__ */ new Set([ require_shared_style_constants.QUOTE_STYLE_ID, require_shared_style_constants.CODE_BLOCK_STYLE_ID, require_shared_style_constants.HORIZONTAL_RULE_STYLE_ID, - require_shared_style_constants.HTML_PREFORMATTED_STYLE_ID + require_shared_style_constants.HTML_PREFORMATTED_STYLE_ID, + require_shared_style_constants.MATH_BLOCK_STYLE_ID ]); function isQuotableStyle(styleId) { if (styleId === void 0) return false; @@ -52,6 +53,7 @@ function renderParagraphBody(paragraph, context) { return literal.length === 0 ? `${fence}\n${fence}` : `${fence}\n${literal}\n${fence}`; } if (paragraph.styleId === "HTMLPreformatted") return paragraph.runs.map((run) => run.text).join(""); + if (paragraph.styleId === "MathBlock") return `$$\n${paragraph.runs.map((run) => run.text).join("")}\n$$`; const headingLevel = paragraph.styleId === void 0 ? void 0 : require_shared_style_constants.parseHeadingStyleId(paragraph.styleId); if (headingLevel !== void 0) { let level = headingLevel; diff --git a/dist/emit/emit.js b/dist/emit/emit.js index ac88b3e..a572407 100644 --- a/dist/emit/emit.js +++ b/dist/emit/emit.js @@ -1,7 +1,7 @@ import "../defaults/defaults.js"; import { MarkdownDiagnosticCodes, MarkdownUnsupportedDocumentKindError, NOOP_MARKDOWN_DIAGNOSTIC_SINK } from "../diagnostics/diagnostics.js"; import { parseListNumId } from "../shared/list-id.js"; -import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, QUOTE_STYLE_ID, parseHeadingStyleId } from "../shared/style-constants.js"; +import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MATH_BLOCK_STYLE_ID, QUOTE_STYLE_ID, parseHeadingStyleId } from "../shared/style-constants.js"; import { emitFrontMatter } from "./front-matter.js"; import { emitRuns } from "./inline.js"; import { emitImage } from "./image.js"; @@ -33,7 +33,8 @@ const QUOTABLE_STYLE_IDS = /* @__PURE__ */ new Set([ QUOTE_STYLE_ID, CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, - HTML_PREFORMATTED_STYLE_ID + HTML_PREFORMATTED_STYLE_ID, + MATH_BLOCK_STYLE_ID ]); function isQuotableStyle(styleId) { if (styleId === void 0) return false; @@ -51,6 +52,7 @@ function renderParagraphBody(paragraph, context) { return literal.length === 0 ? `${fence}\n${fence}` : `${fence}\n${literal}\n${fence}`; } if (paragraph.styleId === "HTMLPreformatted") return paragraph.runs.map((run) => run.text).join(""); + if (paragraph.styleId === "MathBlock") return `$$\n${paragraph.runs.map((run) => run.text).join("")}\n$$`; const headingLevel = paragraph.styleId === void 0 ? void 0 : parseHeadingStyleId(paragraph.styleId); if (headingLevel !== void 0) { let level = headingLevel; diff --git a/dist/emit/inline.cjs b/dist/emit/inline.cjs index cf1325d..ed8d29e 100644 --- a/dist/emit/inline.cjs +++ b/dist/emit/inline.cjs @@ -11,8 +11,6 @@ const ESCAPE_CHARS = /* @__PURE__ */ new Set([ "%", "&", "'", - "(", - ")", "*", "+", ",", @@ -87,6 +85,7 @@ function renderLeaf(run, context) { }); return renderCodeSpan(run.text); } + if (run.fontFamily === "Cambria Math") return `\\(${run.text}\\)`; return escapeMarkdownText(run.text); } const STYLE_KEYS = [ @@ -136,7 +135,7 @@ function renderNestedStyles(runs, depth, context) { return out; } function isPlainAutolink(run) { - if (run.hyperlink === void 0 || run.hyperlink.length === 0 || run.bold === true || run.italic === true || run.strike === true || run.fontFamily === "Courier New") return false; + if (run.hyperlink === void 0 || run.hyperlink.length === 0 || run.bold === true || run.italic === true || run.strike === true || run.fontFamily === "Courier New" || run.fontFamily === "Cambria Math") return false; return run.text === run.hyperlink || run.hyperlink === `mailto:${run.text}`; } function escapeLinkDestination(destination) { diff --git a/dist/emit/inline.d.cts b/dist/emit/inline.d.cts index e90cc3a..70eb62c 100644 --- a/dist/emit/inline.d.cts +++ b/dist/emit/inline.d.cts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.cjs"; import { ContentRun } from "document-schema.js"; //#region src/emit/inline.d.ts interface InlineEmitContext { diff --git a/dist/emit/inline.d.ts b/dist/emit/inline.d.ts index 10cc699..1e80a71 100644 --- a/dist/emit/inline.d.ts +++ b/dist/emit/inline.d.ts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.js"; import { ContentRun } from "document-schema.js"; //#region src/emit/inline.d.ts interface InlineEmitContext { diff --git a/dist/emit/inline.js b/dist/emit/inline.js index f460af3..197e596 100644 --- a/dist/emit/inline.js +++ b/dist/emit/inline.js @@ -10,8 +10,6 @@ const ESCAPE_CHARS = /* @__PURE__ */ new Set([ "%", "&", "'", - "(", - ")", "*", "+", ",", @@ -86,6 +84,7 @@ function renderLeaf(run, context) { }); return renderCodeSpan(run.text); } + if (run.fontFamily === "Cambria Math") return `\\(${run.text}\\)`; return escapeMarkdownText(run.text); } const STYLE_KEYS = [ @@ -135,7 +134,7 @@ function renderNestedStyles(runs, depth, context) { return out; } function isPlainAutolink(run) { - if (run.hyperlink === void 0 || run.hyperlink.length === 0 || run.bold === true || run.italic === true || run.strike === true || run.fontFamily === "Courier New") return false; + if (run.hyperlink === void 0 || run.hyperlink.length === 0 || run.bold === true || run.italic === true || run.strike === true || run.fontFamily === "Courier New" || run.fontFamily === "Cambria Math") return false; return run.text === run.hyperlink || run.hyperlink === `mailto:${run.text}`; } function escapeLinkDestination(destination) { diff --git a/dist/emit/table.d.cts b/dist/emit/table.d.cts index 3781a03..92bf106 100644 --- a/dist/emit/table.d.cts +++ b/dist/emit/table.d.cts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.cjs"; import { InlineEmitContext } from "./inline.cjs"; import { ContentTable } from "document-schema.js"; //#region src/emit/table.d.ts diff --git a/dist/emit/table.d.ts b/dist/emit/table.d.ts index 1361cb3..f55e39c 100644 --- a/dist/emit/table.d.ts +++ b/dist/emit/table.d.ts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.js"; import { InlineEmitContext } from "./inline.js"; import { ContentTable } from "document-schema.js"; //#region src/emit/table.d.ts diff --git a/dist/html/render.cjs b/dist/html/render.cjs index 6ec825c..95202f4 100644 --- a/dist/html/render.cjs +++ b/dist/html/render.cjs @@ -79,6 +79,7 @@ function renderInline(node) { case "rawHtml": return node.literal; case "hardBreak": return "
\n"; case "softBreak": return "\n"; + case "mathInline": return `\\(${escapeHtml(node.literal)}\\)`; } } function renderInlines(nodes) { @@ -134,6 +135,11 @@ var HtmlRenderer = class { case "table": this.renderTable(node); return; + case "mathBlock": + this.cr(); + this.out += `$$\n${escapeHtml(node.literal)}\n$$\n`; + this.cr(); + return; case "document": case "listItem": case "tableRow": diff --git a/dist/html/render.d.cts b/dist/html/render.d.cts index b6ac44c..a0b7dcc 100644 --- a/dist/html/render.d.cts +++ b/dist/html/render.d.cts @@ -1,4 +1,4 @@ -import { g as MarkdownInlineNode, s as MarkdownDocumentNode } from "../ast-BXYwy08e.cjs"; +import { g as MarkdownInlineNode, s as MarkdownDocumentNode } from "../ast-DbjiuYr8.cjs"; //#region src/html/render.d.ts declare function escapeHtml(text: string): string; declare function escapeHref(href: string): string; diff --git a/dist/html/render.d.ts b/dist/html/render.d.ts index 73bfd56..d00d889 100644 --- a/dist/html/render.d.ts +++ b/dist/html/render.d.ts @@ -1,4 +1,4 @@ -import { g as MarkdownInlineNode, s as MarkdownDocumentNode } from "../ast-BXYwy08e.js"; +import { g as MarkdownInlineNode, s as MarkdownDocumentNode } from "../ast-DbjiuYr8.js"; //#region src/html/render.d.ts declare function escapeHtml(text: string): string; declare function escapeHref(href: string): string; diff --git a/dist/html/render.js b/dist/html/render.js index 7bfa42a..0efc238 100644 --- a/dist/html/render.js +++ b/dist/html/render.js @@ -78,6 +78,7 @@ function renderInline(node) { case "rawHtml": return node.literal; case "hardBreak": return "
\n"; case "softBreak": return "\n"; + case "mathInline": return `\\(${escapeHtml(node.literal)}\\)`; } } function renderInlines(nodes) { @@ -133,6 +134,11 @@ var HtmlRenderer = class { case "table": this.renderTable(node); return; + case "mathBlock": + this.cr(); + this.out += `$$\n${escapeHtml(node.literal)}\n$$\n`; + this.cr(); + return; case "document": case "listItem": case "tableRow": diff --git a/dist/index.d.cts b/dist/index.d.cts index 364aa5c..962b449 100644 --- a/dist/index.d.cts +++ b/dist/index.d.cts @@ -1,4 +1,4 @@ -import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnsupportedDocumentKindError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownWriteError } from "./diagnostics-DmuWc7d8.cjs"; +import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnsupportedDocumentKindError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownWriteError } from "./diagnostics-B72W0P_E.cjs"; import { MarkdownBytesSchema, markdownCodec } from "./codec.cjs"; import { n as MarkdownImageResolver, r as MarkdownResolvedImageBytes, t as MarkdownImageResolveContext } from "./image-C4KYmz_L.cjs"; import { MarkdownBulletListMarker, MarkdownCodeFenceChar, MarkdownEmphasisMarker, MarkdownHeadingStyle, MarkdownLineEnding, MarkdownOrderedListDelimiter, MarkdownThematicBreakChar, ReadMarkdownOptions, WriteMarkdownOptions, WriteMarkdownStyleOptions } from "./options/options.cjs"; diff --git a/dist/index.d.ts b/dist/index.d.ts index 900ce02..b0c5846 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,4 +1,4 @@ -import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnsupportedDocumentKindError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownWriteError } from "./diagnostics-DmuWc7d8.js"; +import { a as MarkdownInputTooLargeError, c as MarkdownParseError, d as NOOP_MARKDOWN_DIAGNOSTIC_SINK, i as MarkdownDiagnosticSink, l as MarkdownUnsupportedDocumentKindError, n as MarkdownDiagnosticCodes, o as MarkdownInvalidUtf8Error, r as MarkdownDiagnosticSeverity, s as MarkdownNestingLimitExceededError, t as MarkdownDiagnostic, u as MarkdownWriteError } from "./diagnostics-B72W0P_E.js"; import { MarkdownBytesSchema, markdownCodec } from "./codec.js"; import { n as MarkdownImageResolver, r as MarkdownResolvedImageBytes, t as MarkdownImageResolveContext } from "./image-Cm3hT5PS.js"; import { MarkdownBulletListMarker, MarkdownCodeFenceChar, MarkdownEmphasisMarker, MarkdownHeadingStyle, MarkdownLineEnding, MarkdownOrderedListDelimiter, MarkdownThematicBreakChar, ReadMarkdownOptions, WriteMarkdownOptions, WriteMarkdownStyleOptions } from "./options/options.js"; diff --git a/dist/inline-B50cM6cn.d.ts b/dist/inline-TuBQ2TUr.d.ts similarity index 86% rename from dist/inline-B50cM6cn.d.ts rename to dist/inline-TuBQ2TUr.d.ts index 4333038..4c489f8 100644 --- a/dist/inline-B50cM6cn.d.ts +++ b/dist/inline-TuBQ2TUr.d.ts @@ -1,4 +1,4 @@ -import { g as MarkdownInlineNode } from "./ast-BXYwy08e.js"; +import { g as MarkdownInlineNode } from "./ast-DbjiuYr8.js"; import { n as LinkReferenceMap } from "./link-Dv4kxVjk.js"; //#region src/inline/inline.d.ts interface InlineParseOptions { diff --git a/dist/inline-C9YEGDV5.d.cts b/dist/inline-uVHJ5xzT.d.cts similarity index 86% rename from dist/inline-C9YEGDV5.d.cts rename to dist/inline-uVHJ5xzT.d.cts index 1870703..7cfda50 100644 --- a/dist/inline-C9YEGDV5.d.cts +++ b/dist/inline-uVHJ5xzT.d.cts @@ -1,4 +1,4 @@ -import { g as MarkdownInlineNode } from "./ast-BXYwy08e.cjs"; +import { g as MarkdownInlineNode } from "./ast-DbjiuYr8.cjs"; import { n as LinkReferenceMap } from "./link-Dv4kxVjk.cjs"; //#region src/inline/inline.d.ts interface InlineParseOptions { diff --git a/dist/inline/inline.cjs b/dist/inline/inline.cjs index 2b5f272..862f7ea 100644 --- a/dist/inline/inline.cjs +++ b/dist/inline/inline.cjs @@ -6,6 +6,7 @@ const require_inline_delimiter = require("./delimiter.cjs"); const require_inline_node = require("./node.cjs"); const require_inline_gfm_autolink = require("./gfm-autolink.cjs"); const require_inline_link = require("./link.cjs"); +const require_inline_math = require("./math.cjs"); //#region src/inline/inline.ts const PLAIN_TEXT_PATTERN = /[^\n`[\]\\!<&*_~]+/y; const URI_AUTOLINK_PATTERN = /<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>]*>/y; @@ -102,6 +103,7 @@ var InlineParser = class { while (this.text.charAt(this.pos) === " ") this.pos += 1; } parseBackslash() { + const backslashIndex = this.pos; this.pos += 1; const next = this.text.charAt(this.pos); if (next === "\n") { @@ -109,6 +111,16 @@ var InlineParser = class { this.container.appendChild(new require_inline_node.InlineNode("hardBreak")); return; } + if (next === "(") { + const span = require_inline_math.matchMathInlineSpan(this.text, backslashIndex); + if (span !== void 0) { + const node = new require_inline_node.InlineNode("mathInline"); + node.literal = span.slice(2, span.length - 2); + this.container.appendChild(node); + this.pos = backslashIndex + span.length; + return; + } + } if (require_inline_chars.isAsciiPunctuation(next)) { this.appendText(next); this.pos += 1; @@ -442,6 +454,10 @@ function toAstNode(node) { raw: node.raw, value: node.literal }; + case "mathInline": return { + type: "mathInline", + literal: node.literal + }; case "container": return; } } diff --git a/dist/inline/inline.d.cts b/dist/inline/inline.d.cts index c76b820..8e667b1 100644 --- a/dist/inline/inline.d.cts +++ b/dist/inline/inline.d.cts @@ -1,2 +1,2 @@ -import { n as parseInlines, t as InlineParseOptions } from "../inline-C9YEGDV5.cjs"; +import { n as parseInlines, t as InlineParseOptions } from "../inline-uVHJ5xzT.cjs"; export { InlineParseOptions, parseInlines }; \ No newline at end of file diff --git a/dist/inline/inline.d.ts b/dist/inline/inline.d.ts index 38566fd..e00e8d3 100644 --- a/dist/inline/inline.d.ts +++ b/dist/inline/inline.d.ts @@ -1,2 +1,2 @@ -import { n as parseInlines, t as InlineParseOptions } from "../inline-B50cM6cn.js"; +import { n as parseInlines, t as InlineParseOptions } from "../inline-TuBQ2TUr.js"; export { InlineParseOptions, parseInlines }; \ No newline at end of file diff --git a/dist/inline/inline.js b/dist/inline/inline.js index f72f598..d74e3af 100644 --- a/dist/inline/inline.js +++ b/dist/inline/inline.js @@ -5,6 +5,7 @@ import { DelimiterStack, isDelimiterChar, processEmphasis, scanDelimiterRun } fr import { InlineNode, createTextNode } from "./node.js"; import { applyGfmAutolinks } from "./gfm-autolink.js"; import { matchLinkLabel, normalizeLinkLabel, parseLinkDestination, parseLinkTitle, skipInlineWhitespace } from "./link.js"; +import { matchMathInlineSpan } from "./math.js"; //#region src/inline/inline.ts const PLAIN_TEXT_PATTERN = /[^\n`[\]\\!<&*_~]+/y; const URI_AUTOLINK_PATTERN = /<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>]*>/y; @@ -101,6 +102,7 @@ var InlineParser = class { while (this.text.charAt(this.pos) === " ") this.pos += 1; } parseBackslash() { + const backslashIndex = this.pos; this.pos += 1; const next = this.text.charAt(this.pos); if (next === "\n") { @@ -108,6 +110,16 @@ var InlineParser = class { this.container.appendChild(new InlineNode("hardBreak")); return; } + if (next === "(") { + const span = matchMathInlineSpan(this.text, backslashIndex); + if (span !== void 0) { + const node = new InlineNode("mathInline"); + node.literal = span.slice(2, span.length - 2); + this.container.appendChild(node); + this.pos = backslashIndex + span.length; + return; + } + } if (isAsciiPunctuation(next)) { this.appendText(next); this.pos += 1; @@ -441,6 +453,10 @@ function toAstNode(node) { raw: node.raw, value: node.literal }; + case "mathInline": return { + type: "mathInline", + literal: node.literal + }; case "container": return; } } diff --git a/dist/inline/math.cjs b/dist/inline/math.cjs new file mode 100644 index 0000000..bfc1304 --- /dev/null +++ b/dist/inline/math.cjs @@ -0,0 +1,10 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); +//#region src/inline/math.ts +function matchMathInlineSpan(text, index) { + if (text.charAt(index) !== "\\" || text.charAt(index + 1) !== "(") return; + const closeIndex = text.indexOf("\\)", index + 2); + if (closeIndex === -1) return; + return text.slice(index, closeIndex + 2); +} +//#endregion +exports.matchMathInlineSpan = matchMathInlineSpan; diff --git a/dist/inline/math.d.cts b/dist/inline/math.d.cts new file mode 100644 index 0000000..e7efe03 --- /dev/null +++ b/dist/inline/math.d.cts @@ -0,0 +1,4 @@ +//#region src/inline/math.d.ts +declare function matchMathInlineSpan(text: string, index: number): string | undefined; +//#endregion +export { matchMathInlineSpan }; \ No newline at end of file diff --git a/dist/inline/math.d.ts b/dist/inline/math.d.ts new file mode 100644 index 0000000..e7efe03 --- /dev/null +++ b/dist/inline/math.d.ts @@ -0,0 +1,4 @@ +//#region src/inline/math.d.ts +declare function matchMathInlineSpan(text: string, index: number): string | undefined; +//#endregion +export { matchMathInlineSpan }; \ No newline at end of file diff --git a/dist/inline/math.js b/dist/inline/math.js new file mode 100644 index 0000000..6f66775 --- /dev/null +++ b/dist/inline/math.js @@ -0,0 +1,9 @@ +//#region src/inline/math.ts +function matchMathInlineSpan(text, index) { + if (text.charAt(index) !== "\\" || text.charAt(index + 1) !== "(") return; + const closeIndex = text.indexOf("\\)", index + 2); + if (closeIndex === -1) return; + return text.slice(index, closeIndex + 2); +} +//#endregion +export { matchMathInlineSpan }; diff --git a/dist/inline/node.d.cts b/dist/inline/node.d.cts index e3bb2c3..5c99ac3 100644 --- a/dist/inline/node.d.cts +++ b/dist/inline/node.d.cts @@ -1,5 +1,5 @@ //#region src/inline/node.d.ts -type InlineNodeKind = 'text' | 'emphasis' | 'strong' | 'strikethrough' | 'codeSpan' | 'link' | 'image' | 'autolink' | 'hardBreak' | 'softBreak' | 'rawHtml' | 'entity' | 'container'; +type InlineNodeKind = 'text' | 'emphasis' | 'strong' | 'strikethrough' | 'codeSpan' | 'link' | 'image' | 'autolink' | 'hardBreak' | 'softBreak' | 'rawHtml' | 'entity' | 'mathInline' | 'container'; declare class InlineNode { readonly kind: InlineNodeKind; literal: string; diff --git a/dist/inline/node.d.ts b/dist/inline/node.d.ts index e3bb2c3..5c99ac3 100644 --- a/dist/inline/node.d.ts +++ b/dist/inline/node.d.ts @@ -1,5 +1,5 @@ //#region src/inline/node.d.ts -type InlineNodeKind = 'text' | 'emphasis' | 'strong' | 'strikethrough' | 'codeSpan' | 'link' | 'image' | 'autolink' | 'hardBreak' | 'softBreak' | 'rawHtml' | 'entity' | 'container'; +type InlineNodeKind = 'text' | 'emphasis' | 'strong' | 'strikethrough' | 'codeSpan' | 'link' | 'image' | 'autolink' | 'hardBreak' | 'softBreak' | 'rawHtml' | 'entity' | 'mathInline' | 'container'; declare class InlineNode { readonly kind: InlineNodeKind; literal: string; diff --git a/dist/lower/front-matter.d.cts b/dist/lower/front-matter.d.cts index 307e1ae..447f7fe 100644 --- a/dist/lower/front-matter.d.cts +++ b/dist/lower/front-matter.d.cts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.cjs"; import { LayoutMetadata } from "document-schema.js"; //#region src/lower/front-matter.d.ts interface FrontMatterResult { diff --git a/dist/lower/front-matter.d.ts b/dist/lower/front-matter.d.ts index 3f21828..38be999 100644 --- a/dist/lower/front-matter.d.ts +++ b/dist/lower/front-matter.d.ts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.js"; import { LayoutMetadata } from "document-schema.js"; //#region src/lower/front-matter.d.ts interface FrontMatterResult { diff --git a/dist/lower/inline.cjs b/dist/lower/inline.cjs index 8f2c11b..9a5d7ad 100644 --- a/dist/lower/inline.cjs +++ b/dist/lower/inline.cjs @@ -46,6 +46,13 @@ function lowerInlineNode(node, style, context) { message: "inline raw HTML was preserved as literal text; it will not be rendered as HTML by any consumer of the resulting ContentDocument" }); return node.literal.length === 0 ? [] : [buildRun(node.literal, style)]; + case "mathInline": + context.sink({ + code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.MATH_INLINE_PRESERVED_AS_TEXT, + severity: "info", + message: "inline math (\\( \\)) was preserved as literal raw LaTeX text; it is not parsed as LaTeX or converted to MathML by this package" + }); + return [buildRun(node.literal, style, require_shared_style_constants.MATH_INLINE_FONT_MARKER)]; case "autolink": { const destination = node.email ? `mailto:${node.destination}` : node.destination; return [buildRun(node.destination, { diff --git a/dist/lower/inline.d.cts b/dist/lower/inline.d.cts index 727cc6d..7caa8ef 100644 --- a/dist/lower/inline.d.cts +++ b/dist/lower/inline.d.cts @@ -1,5 +1,5 @@ -import { g as MarkdownInlineNode } from "../ast-BXYwy08e.cjs"; -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.cjs"; +import { g as MarkdownInlineNode } from "../ast-DbjiuYr8.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.cjs"; import { ContentRun } from "document-schema.js"; //#region src/lower/inline.d.ts interface InlineLowerContext { diff --git a/dist/lower/inline.d.ts b/dist/lower/inline.d.ts index a791948..ccaf816 100644 --- a/dist/lower/inline.d.ts +++ b/dist/lower/inline.d.ts @@ -1,5 +1,5 @@ -import { g as MarkdownInlineNode } from "../ast-BXYwy08e.js"; -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.js"; +import { g as MarkdownInlineNode } from "../ast-DbjiuYr8.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.js"; import { ContentRun } from "document-schema.js"; //#region src/lower/inline.d.ts interface InlineLowerContext { diff --git a/dist/lower/inline.js b/dist/lower/inline.js index f62e409..ccc1f8e 100644 --- a/dist/lower/inline.js +++ b/dist/lower/inline.js @@ -1,5 +1,5 @@ import { MarkdownDiagnosticCodes } from "../diagnostics/diagnostics.js"; -import { MONOSPACE_FONT_FAMILY } from "../shared/style-constants.js"; +import { MATH_INLINE_FONT_MARKER, MONOSPACE_FONT_FAMILY } from "../shared/style-constants.js"; //#region src/lower/inline.ts function buildRun(text, style, fontFamily) { return { @@ -45,6 +45,13 @@ function lowerInlineNode(node, style, context) { message: "inline raw HTML was preserved as literal text; it will not be rendered as HTML by any consumer of the resulting ContentDocument" }); return node.literal.length === 0 ? [] : [buildRun(node.literal, style)]; + case "mathInline": + context.sink({ + code: MarkdownDiagnosticCodes.MATH_INLINE_PRESERVED_AS_TEXT, + severity: "info", + message: "inline math (\\( \\)) was preserved as literal raw LaTeX text; it is not parsed as LaTeX or converted to MathML by this package" + }); + return [buildRun(node.literal, style, MATH_INLINE_FONT_MARKER)]; case "autolink": { const destination = node.email ? `mailto:${node.destination}` : node.destination; return [buildRun(node.destination, { diff --git a/dist/lower/lower.cjs b/dist/lower/lower.cjs index 3960cf7..4f89dcd 100644 --- a/dist/lower/lower.cjs +++ b/dist/lower/lower.cjs @@ -132,6 +132,19 @@ function lowerHtmlBlock(node, context) { styleId: require_shared_style_constants.HTML_PREFORMATTED_STYLE_ID }, context)]; } +function lowerMathBlock(node, context) { + context.sink({ + code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.MATH_BLOCK_PRESERVED_AS_TEXT, + severity: "info", + message: "block math ($$...$$) was preserved as literal raw LaTeX text (styleId \"MathBlock\"); it is not parsed as LaTeX or converted to MathML by this package" + }); + const literal = node.literal.replace(/\n$/, ""); + return [decorateParagraph({ + kind: "paragraph", + runs: literal.length === 0 ? [] : [{ text: literal }], + styleId: require_shared_style_constants.MATH_BLOCK_STYLE_ID + }, context)]; +} function lowerBlockquote(node, context, contentWidthPt) { if (context.quoteDepth >= 1) context.sink({ code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.BLOCKQUOTE_NESTED_DEPTH, @@ -225,6 +238,7 @@ function lowerBlock(node, context, contentWidthPt) { case "codeBlock": return lowerCodeBlock(node, context); case "thematicBreak": return lowerThematicBreak(context); case "htmlBlock": return lowerHtmlBlock(node, context); + case "mathBlock": return lowerMathBlock(node, context); case "table": if (context.list !== void 0) context.sink({ code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.LIST_ITEM_BLOCK_UNLISTED, diff --git a/dist/lower/lower.js b/dist/lower/lower.js index c3eec64..f04577b 100644 --- a/dist/lower/lower.js +++ b/dist/lower/lower.js @@ -2,7 +2,7 @@ import { DEFAULT_MARGINS } from "../defaults/defaults.js"; import { MarkdownDiagnosticCodes, MarkdownInputTooLargeError, NOOP_MARKDOWN_DIAGNOSTIC_SINK } from "../diagnostics/diagnostics.js"; import { parseMarkdown } from "../block/block.js"; import { createNumIdMintState, mintListNumId, mintedListType } from "../shared/list-id.js"; -import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, QUOTE_STYLE_ID, headingStyleId } from "../shared/style-constants.js"; +import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MATH_BLOCK_STYLE_ID, QUOTE_STYLE_ID, headingStyleId } from "../shared/style-constants.js"; import { extractFrontMatter } from "./front-matter.js"; import { resolveMarkdownImage } from "./image.js"; import { lowerCodeBlockRun, lowerInlineNodes } from "./inline.js"; @@ -131,6 +131,19 @@ function lowerHtmlBlock(node, context) { styleId: HTML_PREFORMATTED_STYLE_ID }, context)]; } +function lowerMathBlock(node, context) { + context.sink({ + code: MarkdownDiagnosticCodes.MATH_BLOCK_PRESERVED_AS_TEXT, + severity: "info", + message: "block math ($$...$$) was preserved as literal raw LaTeX text (styleId \"MathBlock\"); it is not parsed as LaTeX or converted to MathML by this package" + }); + const literal = node.literal.replace(/\n$/, ""); + return [decorateParagraph({ + kind: "paragraph", + runs: literal.length === 0 ? [] : [{ text: literal }], + styleId: MATH_BLOCK_STYLE_ID + }, context)]; +} function lowerBlockquote(node, context, contentWidthPt) { if (context.quoteDepth >= 1) context.sink({ code: MarkdownDiagnosticCodes.BLOCKQUOTE_NESTED_DEPTH, @@ -224,6 +237,7 @@ function lowerBlock(node, context, contentWidthPt) { case "codeBlock": return lowerCodeBlock(node, context); case "thematicBreak": return lowerThematicBreak(context); case "htmlBlock": return lowerHtmlBlock(node, context); + case "mathBlock": return lowerMathBlock(node, context); case "table": if (context.list !== void 0) context.sink({ code: MarkdownDiagnosticCodes.LIST_ITEM_BLOCK_UNLISTED, diff --git a/dist/lower/table.d.cts b/dist/lower/table.d.cts index e69a8d1..93750ab 100644 --- a/dist/lower/table.d.cts +++ b/dist/lower/table.d.cts @@ -1,4 +1,4 @@ -import { j as MarkdownTableNode } from "../ast-BXYwy08e.cjs"; +import { N as MarkdownTableNode } from "../ast-DbjiuYr8.cjs"; import { InlineLowerContext } from "./inline.cjs"; import { ContentTable } from "document-schema.js"; //#region src/lower/table.d.ts diff --git a/dist/lower/table.d.ts b/dist/lower/table.d.ts index 0c581c4..b4f8fb8 100644 --- a/dist/lower/table.d.ts +++ b/dist/lower/table.d.ts @@ -1,4 +1,4 @@ -import { j as MarkdownTableNode } from "../ast-BXYwy08e.js"; +import { N as MarkdownTableNode } from "../ast-DbjiuYr8.js"; import { InlineLowerContext } from "./inline.js"; import { ContentTable } from "document-schema.js"; //#region src/lower/table.d.ts diff --git a/dist/options/options.d.cts b/dist/options/options.d.cts index 0fb02c4..babfd18 100644 --- a/dist/options/options.d.cts +++ b/dist/options/options.d.cts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.cjs"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.cjs"; import { n as MarkdownImageResolver } from "../image-C4KYmz_L.cjs"; import { Margins, PageSize } from "document-schema.js"; //#region src/options/options.d.ts diff --git a/dist/options/options.d.ts b/dist/options/options.d.ts index e96ac75..038ad52 100644 --- a/dist/options/options.d.ts +++ b/dist/options/options.d.ts @@ -1,4 +1,4 @@ -import { i as MarkdownDiagnosticSink } from "../diagnostics-DmuWc7d8.js"; +import { i as MarkdownDiagnosticSink } from "../diagnostics-B72W0P_E.js"; import { n as MarkdownImageResolver } from "../image-Cm3hT5PS.js"; import { Margins, PageSize } from "document-schema.js"; //#region src/options/options.d.ts diff --git a/dist/read.d.cts b/dist/read.d.cts index 37c91cd..224715e 100644 --- a/dist/read.d.cts +++ b/dist/read.d.cts @@ -1,4 +1,4 @@ -import { t as MarkdownDiagnostic } from "./diagnostics-DmuWc7d8.cjs"; +import { t as MarkdownDiagnostic } from "./diagnostics-B72W0P_E.cjs"; import { ReadMarkdownOptions } from "./options/options.cjs"; import { ContentDocument } from "document-schema.js"; //#region src/read.d.ts diff --git a/dist/read.d.ts b/dist/read.d.ts index d39ac04..79e4280 100644 --- a/dist/read.d.ts +++ b/dist/read.d.ts @@ -1,4 +1,4 @@ -import { t as MarkdownDiagnostic } from "./diagnostics-DmuWc7d8.js"; +import { t as MarkdownDiagnostic } from "./diagnostics-B72W0P_E.js"; import { ReadMarkdownOptions } from "./options/options.js"; import { ContentDocument } from "document-schema.js"; //#region src/read.d.ts diff --git a/dist/shared/style-constants.cjs b/dist/shared/style-constants.cjs index fef247c..936a19b 100644 --- a/dist/shared/style-constants.cjs +++ b/dist/shared/style-constants.cjs @@ -16,7 +16,9 @@ const QUOTE_STYLE_ID = "Quote"; const CODE_BLOCK_STYLE_ID = "CodeBlock"; const HORIZONTAL_RULE_STYLE_ID = "HorizontalRule"; const HTML_PREFORMATTED_STYLE_ID = "HTMLPreformatted"; +const MATH_BLOCK_STYLE_ID = "MathBlock"; const MONOSPACE_FONT_FAMILY = "Courier New"; +const MATH_INLINE_FONT_MARKER = "Cambria Math"; const QUOTE_INDENT_PT = 36; const TASK_CHECKBOX_UNCHECKED = "☐"; const TASK_CHECKBOX_CHECKED = "☒"; @@ -24,6 +26,8 @@ const TASK_CHECKBOX_CHECKED = "☒"; exports.CODE_BLOCK_STYLE_ID = CODE_BLOCK_STYLE_ID; exports.HORIZONTAL_RULE_STYLE_ID = HORIZONTAL_RULE_STYLE_ID; exports.HTML_PREFORMATTED_STYLE_ID = HTML_PREFORMATTED_STYLE_ID; +exports.MATH_BLOCK_STYLE_ID = MATH_BLOCK_STYLE_ID; +exports.MATH_INLINE_FONT_MARKER = MATH_INLINE_FONT_MARKER; exports.MAX_HEADING_STYLE_LEVEL = MAX_HEADING_STYLE_LEVEL; exports.MONOSPACE_FONT_FAMILY = MONOSPACE_FONT_FAMILY; exports.QUOTE_INDENT_PT = QUOTE_INDENT_PT; diff --git a/dist/shared/style-constants.d.cts b/dist/shared/style-constants.d.cts index f1eaa60..8ce1b0a 100644 --- a/dist/shared/style-constants.d.cts +++ b/dist/shared/style-constants.d.cts @@ -6,9 +6,11 @@ declare const QUOTE_STYLE_ID = "Quote"; declare const CODE_BLOCK_STYLE_ID = "CodeBlock"; declare const HORIZONTAL_RULE_STYLE_ID = "HorizontalRule"; declare const HTML_PREFORMATTED_STYLE_ID = "HTMLPreformatted"; +declare const MATH_BLOCK_STYLE_ID = "MathBlock"; declare const MONOSPACE_FONT_FAMILY = "Courier New"; +declare const MATH_INLINE_FONT_MARKER = "Cambria Math"; declare const QUOTE_INDENT_PT = 36; declare const TASK_CHECKBOX_UNCHECKED = "☐"; declare const TASK_CHECKBOX_CHECKED = "☒"; //#endregion -export { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, headingStyleId, parseHeadingStyleId }; \ No newline at end of file +export { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MATH_BLOCK_STYLE_ID, MATH_INLINE_FONT_MARKER, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, headingStyleId, parseHeadingStyleId }; \ No newline at end of file diff --git a/dist/shared/style-constants.d.ts b/dist/shared/style-constants.d.ts index f1eaa60..8ce1b0a 100644 --- a/dist/shared/style-constants.d.ts +++ b/dist/shared/style-constants.d.ts @@ -6,9 +6,11 @@ declare const QUOTE_STYLE_ID = "Quote"; declare const CODE_BLOCK_STYLE_ID = "CodeBlock"; declare const HORIZONTAL_RULE_STYLE_ID = "HorizontalRule"; declare const HTML_PREFORMATTED_STYLE_ID = "HTMLPreformatted"; +declare const MATH_BLOCK_STYLE_ID = "MathBlock"; declare const MONOSPACE_FONT_FAMILY = "Courier New"; +declare const MATH_INLINE_FONT_MARKER = "Cambria Math"; declare const QUOTE_INDENT_PT = 36; declare const TASK_CHECKBOX_UNCHECKED = "☐"; declare const TASK_CHECKBOX_CHECKED = "☒"; //#endregion -export { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, headingStyleId, parseHeadingStyleId }; \ No newline at end of file +export { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MATH_BLOCK_STYLE_ID, MATH_INLINE_FONT_MARKER, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, headingStyleId, parseHeadingStyleId }; \ No newline at end of file diff --git a/dist/shared/style-constants.js b/dist/shared/style-constants.js index 8ca7fda..8e2645f 100644 --- a/dist/shared/style-constants.js +++ b/dist/shared/style-constants.js @@ -15,9 +15,11 @@ const QUOTE_STYLE_ID = "Quote"; const CODE_BLOCK_STYLE_ID = "CodeBlock"; const HORIZONTAL_RULE_STYLE_ID = "HorizontalRule"; const HTML_PREFORMATTED_STYLE_ID = "HTMLPreformatted"; +const MATH_BLOCK_STYLE_ID = "MathBlock"; const MONOSPACE_FONT_FAMILY = "Courier New"; +const MATH_INLINE_FONT_MARKER = "Cambria Math"; const QUOTE_INDENT_PT = 36; const TASK_CHECKBOX_UNCHECKED = "☐"; const TASK_CHECKBOX_CHECKED = "☒"; //#endregion -export { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, headingStyleId, parseHeadingStyleId }; +export { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MATH_BLOCK_STYLE_ID, MATH_INLINE_FONT_MARKER, MAX_HEADING_STYLE_LEVEL, MONOSPACE_FONT_FAMILY, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, headingStyleId, parseHeadingStyleId }; diff --git a/src/ast/ast.ts b/src/ast/ast.ts index 8253b49..05eace7 100644 --- a/src/ast/ast.ts +++ b/src/ast/ast.ts @@ -26,7 +26,8 @@ export type MarkdownBlockNode = | MarkdownHtmlBlockNode | MarkdownTableNode | MarkdownTableRowNode - | MarkdownTableCellNode; + | MarkdownTableCellNode + | MarkdownMathBlockNode; export interface MarkdownDocumentNode { readonly type: 'document'; @@ -130,6 +131,13 @@ export interface MarkdownTableCellNode { readonly position?: MarkdownPosition; } +// Pandoc/GitHub math-extension display math: a $$ line, raw LaTeX content, a closing $$ line (ExaDev/markdown-codec#53). Modelled on MarkdownCodeBlockNode's own fenced convention -- literal is the content BETWEEN the two delimiter lines, never including them (src/block/block.ts's tryMathBlockStart/finalizeMathBlock regenerate a fresh $$ pair on the way back out, exactly as a fenced code block regenerates its own fence rather than preserving the original). Never parsed as LaTeX or converted to MathML by this package -- src/lower/lower.ts preserves it as literal text; that conversion is a documents.js question (ExaDev/documents.js#563). +export interface MarkdownMathBlockNode { + readonly type: 'mathBlock'; + readonly literal: string; + readonly position?: MarkdownPosition; +} + // --- Inline nodes --- export type MarkdownInlineNode = @@ -144,7 +152,8 @@ export type MarkdownInlineNode = | MarkdownHardBreakNode | MarkdownSoftBreakNode | MarkdownRawHtmlNode - | MarkdownEntityNode; + | MarkdownEntityNode + | MarkdownMathInlineNode; export type MarkdownEmphasisMarker = '_' | '*'; @@ -233,6 +242,13 @@ export interface MarkdownEntityNode { readonly position?: MarkdownPosition; } +// Pandoc/GitHub math-extension inline math: \( \) (ExaDev/markdown-codec#53). Deliberately NOT single-dollar $...$ -- the classic currency false-positive failure mode. Modelled on MarkdownCodeSpanNode's own convention: literal is the INNER LaTeX only, \( and \) excluded -- src/lower/inline.ts marks the lowered run with a dedicated ContentRun.fontFamily (MATH_INLINE_FONT_MARKER, src/shared/style-constants.ts, the same opportunistic-reuse trick a code span's own Courier New marker already plays) rather than folding the delimiters into the run's own text, because escapeMarkdownText (src/emit/inline.ts) backslash-escapes literal '(' and ')' in ORDINARY text -- a self-describing "\(...\) in the text is always math" rule would misrecognise any ordinary escaped parenthetical remark as math on reparse. Never parsed as LaTeX or converted to MathML by this package -- that conversion is a documents.js question (ExaDev/documents.js#563). +export interface MarkdownMathInlineNode { + readonly type: 'mathInline'; + readonly literal: string; + readonly position?: MarkdownPosition; +} + const BLOCK_NODE_TYPES: ReadonlySet = new Set([ 'document', 'paragraph', @@ -246,6 +262,7 @@ const BLOCK_NODE_TYPES: ReadonlySet = new Set 'table', 'tableRow', 'tableCell', + 'mathBlock', ]); export function isMarkdownBlockNode(node: MarkdownNode): node is MarkdownBlockNode { diff --git a/src/block/block.test.ts b/src/block/block.test.ts index d5e4071..d85d4de 100644 --- a/src/block/block.test.ts +++ b/src/block/block.test.ts @@ -41,6 +41,27 @@ describe('code blocks', () => { }); }); +describe('math blocks (ExaDev/markdown-codec#53)', () => { + it('records the content between two $$ lines, delimiters excluded', () => { + expect(parse('$$\nx^2\n$$')).toEqual([{ type: 'mathBlock', literal: 'x^2\n' }]); + }); + + it('interrupts an open paragraph, like a code fence', () => { + expect(parse('foo\n$$\nx^2\n$$')).toEqual([ + { type: 'paragraph', children: [{ type: 'text', value: 'foo' }] }, + { type: 'mathBlock', literal: 'x^2\n' }, + ]); + }); + + it('records an empty block when the closing $$ follows immediately', () => { + expect(parse('$$\n$$')).toEqual([{ type: 'mathBlock', literal: '' }]); + }); + + it('does not close on a line that is $$ plus other content -- only a bare $$ line closes', () => { + expect(parse('$$\nx^2 $$ y\n$$')).toEqual([{ type: 'mathBlock', literal: 'x^2 $$ y\n' }]); + }); +}); + describe('lists', () => { it('records the bullet character a list was written with', () => { expect(parse('+ foo')).toEqual([ @@ -214,4 +235,10 @@ describe('recover-tier diagnostics', () => { parseMarkdown('[foo]: /first\n[foo]: /second', { sink: collector.sink }); expect(collector.has(MarkdownDiagnosticCodes.DUPLICATE_LINK_REFERENCE)).toBe(true); }); + + it('reports a math block never closed by a matching $$ before end-of-input', () => { + const collector = createDiagnosticCollector(); + parseMarkdown('$$\nx^2', { sink: collector.sink }); + expect(collector.has(MarkdownDiagnosticCodes.UNCLOSED_MATH_BLOCK)).toBe(true); + }); }); diff --git a/src/block/block.ts b/src/block/block.ts index ef2a8e2..28edf71 100644 --- a/src/block/block.ts +++ b/src/block/block.ts @@ -52,8 +52,8 @@ const NUL_PATTERN = /\0/g; const LINE_ENDING_PATTERN = /\r\n|\n|\r/; -// A cheap first filter before the block-start list is tried at all: no block start, and no paragraph promotion, can begin with any other character. `|` and `:` are here for the GFM table delimiter row (`| --- |`, `:-: | ---:`), the only construct in this package that can start with either. -const MAYBE_SPECIAL_PATTERN = /^[#`~*+_=<>0-9|:-]/; +// A cheap first filter before the block-start list is tried at all: no block start, and no paragraph promotion, can begin with any other character. `|` and `:` are here for the GFM table delimiter row (`| --- |`, `:-: | ---:`), the only construct in this package that can start with either. `$` is here for a $$ math block's own opening line (ExaDev/markdown-codec#53). +const MAYBE_SPECIAL_PATTERN = /^[#$`~*+_=<>0-9|:-]/; // spec 0.31.2, "ATX headings": one to six `#` characters, followed by spaces/tabs or the end of the line. const ATX_MARKER_PATTERN = /^#{1,6}(?:[ \t]+|$)/; @@ -64,6 +64,10 @@ const ATX_TRAILING_CLOSING_SEQUENCE_PATTERN = /[ \t]+#+[ \t]*$/; const CODE_FENCE_PATTERN = /^`{3,}(?!.*`)|^~{3,}/; const CLOSING_CODE_FENCE_PATTERN = /^(?:`{3,}|~{3,})(?=[ \t]*$)/; +// Pandoc/GitHub math-extension display math (ExaDev/markdown-codec#53): a line consisting of exactly $$, optionally followed by trailing spaces/tabs and nothing else -- deliberately stricter than the code-fence pattern above (no "info string", no variable length): both the opening and the closing line must match this exact shape, which is what makes a bare "$$" line on its own unambiguous rather than colliding with GFM's own single-dollar-free inline math (this package never adds inline $$ recognition at all, only \( \)). +const MATH_BLOCK_MARKER_PATTERN = /^\$\$[ \t]*$/; +const MATH_BLOCK_MARKER_LENGTH = 2; + // spec 0.31.2, "Setext headings": a sequence of `=` or of `-`, optionally followed by spaces/tabs, and nothing else. const SETEXT_UNDERLINE_PATTERN = /^(?:=+|-+)[ \t]*$/; @@ -171,6 +175,10 @@ class BlockParser { } if (node.kind === 'htmlBlock' && !HTML_BLOCK_BLANK_LINE_END_TYPES.includes(node.htmlBlockType)) { this.sink({ code: MarkdownDiagnosticCodes.UNTERMINATED_HTML_BLOCK, severity: 'warning', message: `HTML block (type ${String(node.htmlBlockType)}) starting at line ${String(node.startLine)} never met its own end condition before the end of the document`, line: node.startLine }); + return; + } + if (node.kind === 'mathBlock') { + this.sink({ code: MarkdownDiagnosticCodes.UNCLOSED_MATH_BLOCK, severity: 'warning', message: `math block starting at line ${String(node.startLine)} was never closed by a matching closing $$ before the end of the document`, line: node.startLine }); } } @@ -221,6 +229,8 @@ class BlockParser { return this.continueListItem(node); case 'codeBlock': return this.continueCodeBlock(node); + case 'mathBlock': + return this.continueMathBlock(node); case 'htmlBlock': // Types 1-5 end on a line whose own text meets their end condition, checked once that line's text has been added (see addTextToContainer); types 6 and 7 end at a blank line instead. return this.line.blank && HTML_BLOCK_BLANK_LINE_END_TYPES.includes(node.htmlBlockType) ? 'not-matched' : 'matched'; @@ -301,6 +311,15 @@ class BlockParser { return 'matched'; } + // A closing $$ line is never added to the block's own content (matching continueCodeBlock's own closing-fence handling) -- finalize runs directly off the line the closer matched, and the line's processing ends there ('finished'). + private continueMathBlock(node: BlockNode): ContinueResult { + if (!this.line.indented && MATH_BLOCK_MARKER_PATTERN.test(this.line.restFromNextNonspace())) { + this.finalize(node); + return 'finished'; + } + return 'matched'; + } + // Step 2: try block starts against the deepest matched container until one produces a leaf block, none matches, or the line is plainly ordinary text. private openNewBlocks(matchedContainer: BlockNode): BlockNode { let container = matchedContainer; @@ -329,6 +348,7 @@ class BlockParser { () => this.tryBlockquoteStart(), () => this.tryAtxHeadingStart(), () => this.tryCodeFenceStart(), + () => this.tryMathBlockStart(), () => this.tryHtmlBlockStart(container), () => this.tryPromoteParagraph(container), () => this.tryThematicBreakStart(), @@ -396,6 +416,21 @@ class BlockParser { return 'leaf'; } + // A $$ line -- the whole line, nothing else (MATH_BLOCK_MARKER_PATTERN) -- opens a math block, interrupting an open paragraph exactly as a code fence does. The cursor advances past "$$" only, not to end of line, leaving whatever (should only be trailing whitespace) remains as the block's own first content line -- finalizeMathBlock strips that first line back off, mirroring finalizeCodeBlock's own info-string slot. + private tryMathBlockStart(): BlockStartResult { + if (this.line.indented) { + return 'none'; + } + if (!MATH_BLOCK_MARKER_PATTERN.test(this.line.restFromNextNonspace())) { + return 'none'; + } + this.closeUnmatchedBlocks(); + this.addChild('mathBlock'); + this.line.advanceToNextNonspace(); + this.line.advance(MATH_BLOCK_MARKER_LENGTH); + return 'leaf'; + } + private tryHtmlBlockStart(container: BlockNode): BlockStartResult { if (this.line.indented || this.line.peekNextNonspace() !== '<') { return 'none'; @@ -605,6 +640,9 @@ class BlockParser { case 'codeBlock': this.finalizeCodeBlock(node); return; + case 'mathBlock': + this.finalizeMathBlock(node); + return; case 'htmlBlock': node.literal = node.content.replace(TRAILING_HTML_BLANK_LINES_PATTERN, ''); return; @@ -626,6 +664,12 @@ class BlockParser { node.infoString = unescapeString(node.content.slice(0, breakIndex).trim()); node.literal = node.content.slice(breakIndex + 1); } + + // Mirrors finalizeCodeBlock's own fenced branch: the opening "$$" line's own (whitespace-only) remainder is always present as content's first line -- see tryMathBlockStart -- and is stripped off here the same way an opening fence's info-string line is. + private finalizeMathBlock(node: BlockNode): void { + const breakIndex = node.content.indexOf('\n'); + node.literal = breakIndex === -1 ? '' : node.content.slice(breakIndex + 1); + } } function toInlineChildren(content: string, references: LinkReferenceMap, options: MarkdownParseOptions): MarkdownInlineNode[] { @@ -708,6 +752,8 @@ function toAstBlock(node: BlockNode, references: LinkReferenceMap, options: Mark return { type: 'htmlBlock', literal: node.literal }; case 'thematicBreak': return { type: 'thematicBreak' }; + case 'mathBlock': + return { type: 'mathBlock', literal: node.literal }; case 'table': return toTableNode(node, references, options); case 'document': diff --git a/src/block/node.ts b/src/block/node.ts index 4543804..69ae30b 100644 --- a/src/block/node.ts +++ b/src/block/node.ts @@ -15,7 +15,8 @@ export type BlockNodeKind = | 'codeBlock' | 'htmlBlock' | 'thematicBreak' - | 'table'; + | 'table' + | 'mathBlock'; export type BlockHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; @@ -127,7 +128,7 @@ export function canContain(parent: BlockNodeKind, child: BlockNodeKind): boolean } } -// Whether a block accepts raw source lines as its own content. A paragraph and a GFM table accept lines AND still let new block starts be tried against each line (a `>` after a table opens a blockquote and breaks the table); a code block and an HTML block accept lines and suppress block starts entirely, which is what makes their content literal. +// Whether a block accepts raw source lines as its own content. A paragraph and a GFM table accept lines AND still let new block starts be tried against each line (a `>` after a table opens a blockquote and breaks the table); a code block, an HTML block, and a math block accept lines and suppress block starts entirely, which is what makes their content literal. export function acceptsLines(kind: BlockNodeKind): boolean { - return kind === 'paragraph' || kind === 'codeBlock' || kind === 'htmlBlock' || kind === 'table'; + return kind === 'paragraph' || kind === 'codeBlock' || kind === 'htmlBlock' || kind === 'table' || kind === 'mathBlock'; } diff --git a/src/diagnostics/diagnostics.test.ts b/src/diagnostics/diagnostics.test.ts index 761a915..0761ff6 100644 --- a/src/diagnostics/diagnostics.test.ts +++ b/src/diagnostics/diagnostics.test.ts @@ -121,6 +121,27 @@ describe('every MarkdownDiagnosticCodes entry is reachable from real input', () reached.add(MarkdownDiagnosticCodes.RAW_HTML_DROPPED); }); + it('MATH_BLOCK_PRESERVED_AS_TEXT: a $$ display math block', () => { + const collector = createDiagnosticCollector(); + lowerMarkdown('$$\nx^2\n$$', { sink: collector.sink }); + expect(collector.has(MarkdownDiagnosticCodes.MATH_BLOCK_PRESERVED_AS_TEXT)).toBe(true); + reached.add(MarkdownDiagnosticCodes.MATH_BLOCK_PRESERVED_AS_TEXT); + }); + + it('MATH_INLINE_PRESERVED_AS_TEXT: an inline \\( \\) math span', () => { + const collector = createDiagnosticCollector(); + lowerMarkdown('\\(x^2\\)', { sink: collector.sink }); + expect(collector.has(MarkdownDiagnosticCodes.MATH_INLINE_PRESERVED_AS_TEXT)).toBe(true); + reached.add(MarkdownDiagnosticCodes.MATH_INLINE_PRESERVED_AS_TEXT); + }); + + it('UNCLOSED_MATH_BLOCK: a $$ math block never closed before end-of-input', () => { + const collector = createDiagnosticCollector(); + parseMarkdown('$$\nx^2', { sink: collector.sink }); + expect(collector.has(MarkdownDiagnosticCodes.UNCLOSED_MATH_BLOCK)).toBe(true); + reached.add(MarkdownDiagnosticCodes.UNCLOSED_MATH_BLOCK); + }); + it('FRONT_MATTER_KEY_UNMAPPED: an unrecognised front matter key', () => { const collector = createDiagnosticCollector(); lowerMarkdown('---\nunknown: x\n---\n\nbody', { sink: collector.sink, frontMatter: true }); diff --git a/src/diagnostics/diagnostics.ts b/src/diagnostics/diagnostics.ts index b9dc3db..a2e98cf 100644 --- a/src/diagnostics/diagnostics.ts +++ b/src/diagnostics/diagnostics.ts @@ -26,6 +26,7 @@ export const NOOP_MARKDOWN_DIAGNOSTIC_SINK: MarkdownDiagnosticSink = () => { // Degrade tier: an individual construct src/lower's ContentDocument mapping (or src/emit's inverse) cannot represent faithfully, named per the mapping table in the repository's own task history -- see src/lower/ and src/emit/'s own top-of-file comments for the read/write side each code belongs to. Every one of these is reachable from real markdown/ContentDocument input; src/lower.test.ts and src/emit.test.ts each exercise the codes their own stage produces, and src/diagnostics/diagnostics.test.ts asserts the whole MarkdownDiagnosticCodes table has no dead entry. export const MarkdownDiagnosticCodes = { UNCLOSED_FENCE: 'md/unclosed-fence', + UNCLOSED_MATH_BLOCK: 'md/unclosed-math-block', UNTERMINATED_HTML_BLOCK: 'md/unterminated-html-block', TABLE_CELL_COUNT_MISMATCH: 'md/table-cell-count-mismatch', DUPLICATE_LINK_REFERENCE: 'md/duplicate-link-reference', @@ -41,6 +42,8 @@ export const MarkdownDiagnosticCodes = { IMAGE_UNRESOLVED: 'md/image-unresolved', RAW_HTML_PRESERVED_AS_TEXT: 'md/raw-html-preserved-as-text', RAW_HTML_DROPPED: 'md/raw-html-dropped', + MATH_BLOCK_PRESERVED_AS_TEXT: 'md/math-block-preserved-as-text', + MATH_INLINE_PRESERVED_AS_TEXT: 'md/math-inline-preserved-as-text', FRONT_MATTER_KEY_UNMAPPED: 'md/front-matter-key-unmapped', // src/emit (write side: ContentDocument -> markdown) HEADING_LEVEL_CLAMPED: 'md/heading-level-clamped', diff --git a/src/emit/emit.test.ts b/src/emit/emit.test.ts index 0279633..e1beefb 100644 --- a/src/emit/emit.test.ts +++ b/src/emit/emit.test.ts @@ -43,6 +43,16 @@ describe('code blocks, thematic breaks, preformatted HTML', () => { }); }); +describe('math (ExaDev/markdown-codec#53)', () => { + it('emits a MathBlock paragraph as a $$ display math block', () => { + expect(emitMarkdown(doc([{ kind: 'paragraph', runs: [{ text: 'x^2' }], styleId: 'MathBlock' }]))).toBe('$$\nx^2\n$$'); + }); + + it('emits a Cambria-Math-marked run with \\( \\) delimiters, unescaped', () => { + expect(emitMarkdown(doc([{ kind: 'paragraph', runs: [{ text: 'f(x) = x^2', fontFamily: 'Cambria Math' }] }]))).toBe('\\(f(x) = x^2\\)'); + }); +}); + describe('blockquotes', () => { it('prefixes "> " once per recovered nesting level, on every line of the body', () => { const fenced = emitMarkdown(doc([{ kind: 'paragraph', runs: [{ text: 'a\nb' }], styleId: 'CodeBlock', indentLeftPt: 72 }])); @@ -138,6 +148,33 @@ describe('round trip through src/lower', () => { const second = lowerMarkdown(markdown); expect(second).toEqual(first); }); + + it('preserves inline math (\\( \\)), delimiters included, across a full lower -> emit -> lower round trip (ExaDev/markdown-codec#53)', () => { + const source = 'before \\(E = mc^2\\) after'; + const first = lowerMarkdown(source); + const markdown = emitMarkdown(first); + expect(markdown).toBe(source); + const second = lowerMarkdown(markdown); + expect(second).toEqual(first); + }); + + it('preserves a $$ display math block across a full lower -> emit -> lower round trip', () => { + const source = '$$\nx^2\n$$'; + const first = lowerMarkdown(source); + const markdown = emitMarkdown(first); + expect(markdown).toBe(source); + const second = lowerMarkdown(markdown); + expect(second).toEqual(first); + }); + + it('does not let ordinary parenthetical text collide with preserved math on a write-then-reread round trip', () => { + const source = 'a link (not a link) trailing'; + const first = lowerMarkdown(source); + const markdown = emitMarkdown(first); + const second = lowerMarkdown(markdown); + expect(second).toEqual(first); + expect(markdown).not.toContain('\\('); + }); }); describe('gaps (MarkdownDiagnosticCodes)', () => { diff --git a/src/emit/emit.ts b/src/emit/emit.ts index 7e7bbca..214da9e 100644 --- a/src/emit/emit.ts +++ b/src/emit/emit.ts @@ -18,7 +18,7 @@ import { DEFAULT_BULLET_LIST_MARKER, DEFAULT_CODE_FENCE_CHAR, DEFAULT_EMPHASIS_M import type { MarkdownHeadingStyle, WriteMarkdownOptions } from '../options/options'; import type { ListNumIdInfo } from '../shared/list-id'; import { parseListNumId } from '../shared/list-id'; -import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MAX_HEADING_STYLE_LEVEL, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, parseHeadingStyleId } from '../shared/style-constants'; +import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MATH_BLOCK_STYLE_ID, MAX_HEADING_STYLE_LEVEL, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, parseHeadingStyleId } from '../shared/style-constants'; import { emitFrontMatter } from './front-matter'; import { emitImage } from './image'; import type { InlineEmitContext } from './inline'; @@ -72,7 +72,7 @@ function codeFenceFor(literal: string, fenceChar: string): string { return fenceChar.repeat(Math.max(MIN_CODE_FENCE_LENGTH, longestRunLength(literal, fenceChar) + 1)); } -const QUOTABLE_STYLE_IDS: ReadonlySet = new Set([QUOTE_STYLE_ID, CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID]); +const QUOTABLE_STYLE_IDS: ReadonlySet = new Set([QUOTE_STYLE_ID, CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MATH_BLOCK_STYLE_ID]); function isQuotableStyle(styleId: string | undefined): boolean { if (styleId === undefined) { @@ -102,6 +102,11 @@ function renderParagraphBody(paragraph: ContentParagraph, context: EmitContext): if (paragraph.styleId === HTML_PREFORMATTED_STYLE_ID) { return paragraph.runs.map((run) => run.text).join(''); } + if (paragraph.styleId === MATH_BLOCK_STYLE_ID) { + // A fresh $$ pair regenerated around the preserved literal -- src/lower/lower.ts's own lowerMathBlock never kept the original delimiter lines either, exactly mirroring how a fenced code block regenerates its own fence (codeFenceFor) rather than preserving the source fence's exact character/length. + const literal = paragraph.runs.map((run) => run.text).join(''); + return `$$\n${literal}\n$$`; + } const headingLevel = paragraph.styleId === undefined ? undefined : parseHeadingStyleId(paragraph.styleId); if (headingLevel !== undefined) { let level = headingLevel; diff --git a/src/emit/inline.ts b/src/emit/inline.ts index 20a90f0..5efbd11 100644 --- a/src/emit/inline.ts +++ b/src/emit/inline.ts @@ -3,20 +3,22 @@ // Adjacent runs with DIFFERENT bold/italic/strike combinations are never wrapped independently and concatenated -- **bold** immediately followed by its own ___nested___ wrap would fuse into one ambiguous five-underscore delimiter run once written out, which is a real correctness bug, not a style nit. renderNestedStyles instead groups the run sequence hierarchically by bold, then by italic, then by strike, producing a properly NESTED wrap (`**bold *nested***`-shaped) whose closing delimiter run CommonMark's own algorithm resolves correctly (a closer consumes only as many delimiters as its innermost opener needs, leaving the rest for the next one out) -- exactly the well-known trick real markdown output already relies on for this exact shape. // // Escaping (escapeMarkdownText) is conservative: every ASCII punctuation character markdown itself gives meaning to is backslash-escaped, UNLESS it is the `<` of a tag src/html/html.ts's own matchHtmlTag would recognise as raw HTML -- exempting exactly that span (not "any `<`") is what lets a paragraph carrying preserved raw HTML (src/lower/inline.ts's own RAW_HTML_PRESERVED_AS_TEXT case) survive a write-then-read round trip AS HTML rather than as escaped literal text. +// +// Preserved inline math (src/lower/inline.ts's own MATH_INLINE_PRESERVED_AS_TEXT case) is NOT given the same text-pattern-based exemption, deliberately: escapeMarkdownText already backslash-escapes every literal '(' and ')' it meets in ORDINARY text (both are in ESCAPE_CHARS below), so an ordinary escaped parenthetical remark -- "\(see below\)" -- is indistinguishable from a genuine preserved \( \) span once escaped, and a pattern-based exemption (tried and reverted) misrecognised the former as the latter on reparse. renderLeaf below instead keys off the run's own MATH_INLINE_FONT_MARKER fontFamily, the same non-pattern-based, opportunistic-reuse trick a code span's own Courier New marker already plays two paragraphs down. import type { ContentRun } from 'document-schema.js'; import type { MarkdownDiagnosticSink } from '../diagnostics/diagnostics'; import { MarkdownDiagnosticCodes } from '../diagnostics/diagnostics'; import { matchHtmlTag } from '../html/html'; -import { MONOSPACE_FONT_FAMILY } from '../shared/style-constants'; +import { MATH_INLINE_FONT_MARKER, MONOSPACE_FONT_FAMILY } from '../shared/style-constants'; export interface InlineEmitContext { readonly sink: MarkdownDiagnosticSink; readonly emphasisMarker: string; } -// Every ASCII punctuation character CommonMark's own backslash-escape grammar recognises (spec 0.31.2, "Backslash escapes") -- escaping any OTHER character is a no-op under that same grammar, so this set is deliberately the full ASCII-punctuation set src/inline/chars.ts already names, not a hand-picked subset of "characters that look dangerous". -const ESCAPE_CHARS: ReadonlySet = new Set(['!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~']); +// Every ASCII punctuation character CommonMark's own backslash-escape grammar recognises (spec 0.31.2, "Backslash escapes") that genuinely needs escaping to round-trip safely, MINUS '(' and ')' (ExaDev/markdown-codec#53): neither carries any special meaning in ordinary running text under CommonMark's own grammar (a paren only means anything as part of an inline link/image's own "(dest)" syntax, immediately after a `]` this package always escapes anyway -- see the following paragraph), so escaping them was always unnecessary defensive punctuation-escaping, not a correctness requirement. Once \( \) inline math exists, that unnecessary escaping becomes actively harmful: it manufactures the exact \(...\) shape genuine preserved math uses out of ANY ordinary parenthetical remark ("(see below)"), which src/inline/inline.ts's own new \( recognition cannot tell apart from real math on a later reparse (this was tried -- keeping '('/')' escaped and instead pattern-matching a "genuine" math span on the way out -- and reverted; see this module's own top-of-file note and src/ast/ast.ts's own MarkdownMathInlineNode comment for why no text-pattern-based fix exists). A `]` immediately followed by an unescaped `(` could in principle be misread as inline link syntax, but that never happens here: this set still escapes `]` unconditionally, so a literal `]` from ordinary text is never emitted bare, and a REAL link/image's own "](dest)" is produced directly by this module's own emitRuns, never by escaping ordinary text. +const ESCAPE_CHARS: ReadonlySet = new Set(['!', '"', '#', '$', '%', '&', "'", '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~']); export function escapeMarkdownText(text: string): string { let out = ''; @@ -74,6 +76,10 @@ function renderLeaf(run: ContentRun, context: InlineEmitContext): string { context.sink({ code: MarkdownDiagnosticCodes.CODE_SPAN_AS_MONOSPACE_RUN, severity: 'info', message: 'a run styled with the Courier New font family is rendered as a code span; a genuinely monospace run from another format is indistinguishable from a real markdown code span on the way back out' }); return renderCodeSpan(run.text); } + if (run.fontFamily === MATH_INLINE_FONT_MARKER) { + // The \( \) delimiters are regenerated fresh around the run's own (unescaped) text -- this run's text is never passed through escapeMarkdownText at all, since it is not "ordinary punctuation that happens to need escaping" but raw LaTeX carried verbatim (see this module's own top-of-file note on why a text-pattern-based recognition of an already-escaped '(...)' cannot distinguish this from ordinary parenthetical prose). + return `\\(${run.text}\\)`; + } return escapeMarkdownText(run.text); } @@ -149,8 +155,8 @@ function renderNestedStyles(runs: readonly ContentRun[], depth: number, context: } function isPlainAutolink(run: ContentRun): boolean { - // An autolink's own <...> form can never be empty (CommonMark's own URI/email autolink grammar both require at least one character between the brackets) -- `<>` is not valid autolink syntax at all and would reparse as literal text, so an empty destination (only reachable via a `[](/url)`-shaped empty-text link whose text happens to equal its own empty destination) must fall through to the ordinary `[text](dest)` form instead. - if (run.hyperlink === undefined || run.hyperlink.length === 0 || run.bold === true || run.italic === true || run.strike === true || run.fontFamily === MONOSPACE_FONT_FAMILY) { + // An autolink's own <...> form can never be empty (CommonMark's own URI/email autolink grammar both require at least one character between the brackets) -- `<>` is not valid autolink syntax at all and would reparse as literal text, so an empty destination (only reachable via a `[](/url)`-shaped empty-text link whose text happens to equal its own empty destination) must fall through to the ordinary `[text](dest)` form instead. A monospace (code-span) or math-marked run is excluded the same way: both need their own dedicated renderLeaf rendering (a code span's backtick fence, math's own \( \) delimiters), never the bare <...> autolink form, however coincidentally their own text might equal the surrounding hyperlink. + if (run.hyperlink === undefined || run.hyperlink.length === 0 || run.bold === true || run.italic === true || run.strike === true || run.fontFamily === MONOSPACE_FONT_FAMILY || run.fontFamily === MATH_INLINE_FONT_MARKER) { return false; } return run.text === run.hyperlink || run.hyperlink === `mailto:${run.text}`; diff --git a/src/html/render.ts b/src/html/render.ts index 83eb5d1..1124bff 100644 --- a/src/html/render.ts +++ b/src/html/render.ts @@ -99,6 +99,9 @@ function renderInline(node: MarkdownInlineNode): string { return '
\n'; case 'softBreak': return '\n'; + case 'mathInline': + // Math (ExaDev/markdown-codec#53) is a Pandoc/GFM extension outside CommonMark/GFM proper -- neither vendored corpus this renderer exists to check against (src/conformance.test.ts, src/gfm-conformance.test.ts) carries a math example, so there is no cmark-produced expected HTML to match here. The \( \) delimiters are reconstructed around the escaped literal -- matching what the real write path (src/emit/inline.ts's renderLeaf) actually produces -- so an EMPTY math span (a genuine, if unlikely, corpus edge case: two backslash escapes sitting directly adjacent, e.g. "\(\)") renders as "\(\)" here too rather than as nothing, keeping this internal oracle consistent with the real writer it exists to cross-check other constructs against. + return `\\(${escapeHtml(node.literal)}\\)`; } } @@ -165,6 +168,12 @@ class HtmlRenderer { case 'table': this.renderTable(node); return; + case 'mathBlock': + // See renderInline's own mathInline case: the $$ delimiters are reconstructed around the escaped literal, matching src/emit/emit.ts's own real MATH_BLOCK_STYLE_ID branch. + this.cr(); + this.out += `$$\n${escapeHtml(node.literal)}\n$$\n`; + this.cr(); + return; case 'document': case 'listItem': case 'tableRow': diff --git a/src/inline/inline.test.ts b/src/inline/inline.test.ts index acc44fa..a43066e 100644 --- a/src/inline/inline.test.ts +++ b/src/inline/inline.test.ts @@ -49,6 +49,33 @@ describe('code spans', () => { }); }); +describe('inline math (ExaDev/markdown-codec#53)', () => { + it('recognises \\( \\) as inline math, literal excluding the delimiters', () => { + expect(parse('\\(E = mc^2\\)')).toEqual([{ type: 'mathInline', literal: 'E = mc^2' }]); + }); + + it('closes at the first \\), no nested-\\(-awareness', () => { + expect(parse('\\(f(x)\\)')).toEqual([{ type: 'mathInline', literal: 'f(x)' }]); + }); + + it('treats an unmatched \\( as today\'s ordinary backslash escape of "("', () => { + expect(parse('\\(no closer here')).toEqual([{ type: 'text', value: '(no closer here' }]); + }); + + it('allows an empty span', () => { + expect(parse('\\(\\)')).toEqual([{ type: 'mathInline', literal: '' }]); + }); + + it('does not recognise single-dollar $...$ or bare $$ as math -- only \\( \\)', () => { + expect(parse('$x^2$')).toEqual([{ type: 'text', value: '$x^2$' }]); + expect(parse('$$x^2$$')).toEqual([{ type: 'text', value: '$$x^2$$' }]); + }); + + it('does not recognise \\( \\) inside a code span -- the span is sliced verbatim, never re-dispatched', () => { + expect(parse('`\\(x\\)`')).toEqual([{ type: 'codeSpan', literal: '\\(x\\)' }]); + }); +}); + describe('emphasis, strong emphasis, and the flanking rules', () => { it('records which marker character produced the emphasis', () => { expect(parse('_foo_')).toEqual([{ type: 'emphasis', marker: '_', children: [{ type: 'text', value: 'foo' }] }]); diff --git a/src/inline/inline.ts b/src/inline/inline.ts index fc0de75..12a197f 100644 --- a/src/inline/inline.ts +++ b/src/inline/inline.ts @@ -19,6 +19,7 @@ import { matchEntity } from './entity'; import { applyGfmAutolinks } from './gfm-autolink'; import type { LinkReferenceMap, ParsedSpan } from './link'; import { matchLinkLabel, normalizeLinkLabel, parseLinkDestination, parseLinkTitle, skipInlineWhitespace } from './link'; +import { matchMathInlineSpan } from './math'; import { InlineNode, createTextNode } from './node'; export interface InlineParseOptions { @@ -176,7 +177,10 @@ class InlineParser { } // spec 0.31.2, "Backslash escapes": a backslash before any ASCII punctuation character escapes it; a backslash before a line ending is a hard break; a backslash before anything else is a literal backslash. + // + // \( is checked FIRST, ahead of the general escape rule (ExaDev/markdown-codec#53) -- \(...\) inline math (src/inline/math.ts's matchMathInlineSpan) is otherwise indistinguishable from an escaped '(' followed, eventually, by an escaped ')', which is exactly the bug this recognition fixes (the delimiters were being silently eaten as ordinary backslash escapes). An unmatched \( (no closing \) anywhere in the block) falls through unchanged to today's escape behaviour, so a genuinely escaped lone '(' is unaffected. private parseBackslash(): void { + const backslashIndex = this.pos; this.pos += 1; const next = this.text.charAt(this.pos); if (next === '\n') { @@ -184,6 +188,17 @@ class InlineParser { this.container.appendChild(new InlineNode('hardBreak')); return; } + if (next === '(') { + const span = matchMathInlineSpan(this.text, backslashIndex); + if (span !== undefined) { + const node = new InlineNode('mathInline'); + // Strip the \( / \) delimiters (two characters each) -- literal is the inner LaTeX only, matching MarkdownCodeSpanNode's own convention (see src/ast/ast.ts's own MarkdownMathInlineNode comment for why this node does NOT keep its delimiters the way MarkdownRawHtmlNode does). + node.literal = span.slice(2, span.length - 2); + this.container.appendChild(node); + this.pos = backslashIndex + span.length; + return; + } + } if (isAsciiPunctuation(next)) { this.appendText(next); this.pos += 1; @@ -543,6 +558,8 @@ function toAstNode(node: InlineNode): MarkdownInlineNode | undefined { return { type: 'rawHtml', literal: node.literal }; case 'entity': return { type: 'entity', raw: node.raw, value: node.literal }; + case 'mathInline': + return { type: 'mathInline', literal: node.literal }; case 'container': return undefined; } diff --git a/src/inline/math.ts b/src/inline/math.ts new file mode 100644 index 0000000..c185293 --- /dev/null +++ b/src/inline/math.ts @@ -0,0 +1,19 @@ +// Inline \( \) math span recognition (ExaDev/markdown-codec#53), mirroring src/html/html.ts's own matchHtmlTag exactly: a bounded RECOGNISER, not a LaTeX parser -- the span's raw content is carried verbatim and never interpreted. +// +// Consumed by the read side only (src/inline/inline.ts's parseBackslash, which dispatches here on \(). The write side does NOT use pattern-matching to recognise a preserved math run on the way back out -- that was tried and reverted: escapeMarkdownText (src/emit/inline.ts) backslash-escapes ordinary literal '(' and ')' characters, so any ordinary parenthetical remark escapes to the identical \(...\) shape a pattern matcher would misrecognise as math. src/lower/inline.ts marks a lowered math run with a dedicated ContentRun.fontFamily instead (MATH_INLINE_FONT_MARKER, src/shared/style-constants.ts), the same non-pattern-based, opportunistic-reuse trick a code span's own Courier New marker already plays. +// +// Deliberately NOT single-dollar $...$ or block-level $$ (block math is its own construct, src/block/block.ts's tryMathBlockStart) -- see this package's own issue #53 for why single-dollar inline is out of scope (the classic currency false-positive). Never recognised inside a code span or a fenced code block, with no exclusion logic needed here at all: a code span's own content is sliced directly by src/inline/inline.ts's parseCodeSpan without ever being re-dispatched through step() (so this scanner never runs over it), and a fenced code block's own literal never reaches inline parsing in the first place (src/block/block.ts's toAstBlock passes a codeBlock's literal straight through with no toInlineChildren call). +// +// Closes at the FIRST literal '\)' -- no nested-\(-awareness and no escape handling inside the span, matching src/inline/inline.ts's own code-span precedent (a code span closes at the first backtick run of the matching length, with nothing inside it re-interpreted). An unmatched \( (no \) anywhere in the remaining text) is not math at all; the caller falls back to today's ordinary backslash-escape reading of a lone \(. +// +// Returns the FULL matched span, delimiters included (e.g. '\(x^2\)') -- the caller strips the two-character \( / \) delimiters off to build MarkdownMathInlineNode's own literal (inner content only, matching MarkdownCodeSpanNode's convention), while using the full span's own length to advance the scan position past it. +export function matchMathInlineSpan(text: string, index: number): string | undefined { + if (text.charAt(index) !== '\\' || text.charAt(index + 1) !== '(') { + return undefined; + } + const closeIndex = text.indexOf('\\)', index + 2); + if (closeIndex === -1) { + return undefined; + } + return text.slice(index, closeIndex + 2); +} diff --git a/src/inline/node.ts b/src/inline/node.ts index b206919..f7ab880 100644 --- a/src/inline/node.ts +++ b/src/inline/node.ts @@ -17,6 +17,7 @@ export type InlineNodeKind = | 'softBreak' | 'rawHtml' | 'entity' + | 'mathInline' // The synthetic root every inline parse builds into -- never converted to an AST node itself, only its children are. | 'container'; diff --git a/src/lower/inline.ts b/src/lower/inline.ts index 955a496..fa64f6b 100644 --- a/src/lower/inline.ts +++ b/src/lower/inline.ts @@ -6,7 +6,7 @@ import type { ContentRun } from 'document-schema.js'; import type { MarkdownInlineNode } from '../ast/ast'; import type { MarkdownDiagnosticSink } from '../diagnostics/diagnostics'; import { MarkdownDiagnosticCodes } from '../diagnostics/diagnostics'; -import { MONOSPACE_FONT_FAMILY } from '../shared/style-constants'; +import { MATH_INLINE_FONT_MARKER, MONOSPACE_FONT_FAMILY } from '../shared/style-constants'; export interface InlineLowerContext { readonly sink: MarkdownDiagnosticSink; @@ -61,6 +61,10 @@ export function lowerInlineNode(node: MarkdownInlineNode, style: RunStyle, conte } context.sink({ code: MarkdownDiagnosticCodes.RAW_HTML_PRESERVED_AS_TEXT, severity: 'info', message: 'inline raw HTML was preserved as literal text; it will not be rendered as HTML by any consumer of the resulting ContentDocument' }); return node.literal.length === 0 ? [] : [buildRun(node.literal, style)]; + case 'mathInline': + // Marked with MATH_INLINE_FONT_MARKER, the same opportunistic-reuse trick a code span's own Courier New marker plays -- src/emit/inline.ts's renderLeaf reconstructs the \( \) delimiters around this run's own text (rather than escaping it as ordinary punctuation) specifically because it carries this marker, not because of anything about the text's own shape (see src/ast/ast.ts's own MarkdownMathInlineNode comment for why a text-pattern-based approach was tried and reverted). + context.sink({ code: MarkdownDiagnosticCodes.MATH_INLINE_PRESERVED_AS_TEXT, severity: 'info', message: 'inline math (\\( \\)) was preserved as literal raw LaTeX text; it is not parsed as LaTeX or converted to MathML by this package' }); + return [buildRun(node.literal, style, MATH_INLINE_FONT_MARKER)]; case 'autolink': { const destination = node.email ? `mailto:${node.destination}` : node.destination; return [buildRun(node.destination, { ...style, hyperlink: destination })]; diff --git a/src/lower/lower.test.ts b/src/lower/lower.test.ts index 2fa9028..029e015 100644 --- a/src/lower/lower.test.ts +++ b/src/lower/lower.test.ts @@ -89,6 +89,24 @@ describe('code spans and code blocks', () => { }); }); +describe('math (ExaDev/markdown-codec#53)', () => { + it('maps inline math to a run marked with the Cambria Math font, delimiters excluded from the run text', () => { + expect(paragraph(blocks('\\(x^2\\)')[0]).runs).toEqual([{ text: 'x^2', fontFamily: 'Cambria Math' }]); + }); + + it('maps a $$ display math block to one MathBlock paragraph', () => { + const block = paragraph(blocks('$$\nx^2\n$$')[0]); + expect(block.styleId).toBe('MathBlock'); + expect(block.runs).toEqual([{ text: 'x^2' }]); + }); + + it('produces an empty-runs paragraph for an empty math block', () => { + const block = paragraph(blocks('$$\n$$')[0]); + expect(block.styleId).toBe('MathBlock'); + expect(block.runs).toEqual([]); + }); +}); + describe('blockquotes', () => { it('maps a blockquote paragraph to styleId Quote plus indentLeftPt', () => { const block = paragraph(blocks('> foo')[0]); @@ -205,6 +223,18 @@ describe('gaps (MarkdownDiagnosticCodes)', () => { expect(collector.has(MarkdownDiagnosticCodes.CODE_BLOCK_INFO_STRING_DROPPED)).toBe(true); }); + it('MATH_BLOCK_PRESERVED_AS_TEXT fires for a $$ display math block', () => { + const collector = createDiagnosticCollector(); + blocks('$$\nx^2\n$$', { sink: collector.sink }); + expect(collector.has(MarkdownDiagnosticCodes.MATH_BLOCK_PRESERVED_AS_TEXT)).toBe(true); + }); + + it('MATH_INLINE_PRESERVED_AS_TEXT fires for an inline \\( \\) math span', () => { + const collector = createDiagnosticCollector(); + blocks('\\(x^2\\)', { sink: collector.sink }); + expect(collector.has(MarkdownDiagnosticCodes.MATH_INLINE_PRESERVED_AS_TEXT)).toBe(true); + }); + it('BLOCKQUOTE_NESTED_DEPTH fires beyond level 1', () => { const collector = createDiagnosticCollector(); blocks('> > nested', { sink: collector.sink }); diff --git a/src/lower/lower.ts b/src/lower/lower.ts index 0072fbb..5a73b3a 100644 --- a/src/lower/lower.ts +++ b/src/lower/lower.ts @@ -10,6 +10,7 @@ // - GFM tables -> ContentTable, src/lower/table.ts. // - images -> ContentImageBlock via a synchronous MarkdownImageResolver port (src/lower/image.ts) -- MarkdownDiagnosticCodes.IMAGE_UNRESOLVED when the resolver (or native data: URI decoding) cannot produce a real PNG/JPEG; the image degrades to a text run of alt text + hyperlink, NEVER an invalid ContentImageBlock. A top-level image (a direct child of a paragraph) splits that paragraph precisely at the point it occurs; a nested one (inside emphasis/a link) never resolves at all -- see src/lower/inline.ts's own top-of-file note. // - raw HTML -> preserved as literal text by default (styleId 'HTMLPreformatted' for block-level HTML), a rawHtml: 'drop' option available -- MarkdownDiagnosticCodes.RAW_HTML_PRESERVED_AS_TEXT / RAW_HTML_DROPPED. +// - $$ display math / \( \) inline math (ExaDev/markdown-codec#53) -> preserved as literal raw LaTeX text (styleId 'MathBlock' for the block form; the inline form keeps its own \( \) delimiters in the run text so src/emit/inline.ts's escapeMarkdownText can recognise and pass it through unescaped -- see src/inline/math.ts) -- MarkdownDiagnosticCodes.MATH_BLOCK_PRESERVED_AS_TEXT / MATH_INLINE_PRESERVED_AS_TEXT. Never parsed as LaTeX or converted to MathML here -- that is a documents.js question (ExaDev/documents.js#563). // - front matter (src/lower/front-matter.ts) -> a flat-scalar-only LayoutMetadata subset -- MarkdownDiagnosticCodes.FRONT_MATTER_KEY_UNMAPPED. import type { ContentBlock, ContentDocument, ContentParagraph, ContentRun, LayoutMetadata } from 'document-schema.js'; @@ -23,7 +24,7 @@ import { MarkdownDiagnosticCodes, MarkdownInputTooLargeError, NOOP_MARKDOWN_DIAG import type { ReadMarkdownOptions } from '../options/options'; import type { NumIdMintState } from '../shared/list-id'; import { createNumIdMintState, mintedListType, mintListNumId } from '../shared/list-id'; -import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, headingStyleId } from '../shared/style-constants'; +import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MATH_BLOCK_STYLE_ID, QUOTE_INDENT_PT, QUOTE_STYLE_ID, TASK_CHECKBOX_CHECKED, TASK_CHECKBOX_UNCHECKED, headingStyleId } from '../shared/style-constants'; import { extractFrontMatter } from './front-matter'; import type { MarkdownImageResolver } from './image'; import { resolveMarkdownImage } from './image'; @@ -138,6 +139,15 @@ function lowerHtmlBlock(node: Extract, return [decorateParagraph(paragraph, context)]; } +// $$...$$ display math (ExaDev/markdown-codec#53) preserved as literal raw LaTeX text, styleId 'MathBlock' -- see src/emit/emit.ts's own inverse. Not parsed as LaTeX or converted to MathML here: that is a documents.js question (ExaDev/documents.js#563), and this package's own scope stops at recognising and round-tripping the syntax. +function lowerMathBlock(node: Extract, context: BlockLowerContext): ContentBlock[] { + context.sink({ code: MarkdownDiagnosticCodes.MATH_BLOCK_PRESERVED_AS_TEXT, severity: 'info', message: 'block math ($$...$$) was preserved as literal raw LaTeX text (styleId "MathBlock"); it is not parsed as LaTeX or converted to MathML by this package' }); + const literal = node.literal.replace(/\n$/, ''); + const runs: ContentRun[] = literal.length === 0 ? [] : [{ text: literal }]; + const paragraph: ContentParagraph = { kind: 'paragraph', runs, styleId: MATH_BLOCK_STYLE_ID }; + return [decorateParagraph(paragraph, context)]; +} + function lowerBlockquote(node: Extract, context: BlockLowerContext, contentWidthPt: number): ContentBlock[] { if (context.quoteDepth >= 1) { context.sink({ code: MarkdownDiagnosticCodes.BLOCKQUOTE_NESTED_DEPTH, severity: 'info', message: `blockquote nesting beyond level 1 is represented only as a larger indentLeftPt (${String((context.quoteDepth + 1) * QUOTE_INDENT_PT)}pt); recovering the exact nesting depth back out is an approximation, not an exact inverse` }); @@ -228,6 +238,8 @@ function lowerBlock(node: MarkdownBlockNode, context: BlockLowerContext, content return lowerThematicBreak(context); case 'htmlBlock': return lowerHtmlBlock(node, context); + case 'mathBlock': + return lowerMathBlock(node, context); case 'table': { if (context.list !== undefined) { context.sink({ code: MarkdownDiagnosticCodes.LIST_ITEM_BLOCK_UNLISTED, severity: 'info', message: 'a table directly inside a list item has no ContentListMembership field of its own -- only ContentParagraph carries .list -- so its association with the enclosing list item is lost' }); diff --git a/src/shared/style-constants.ts b/src/shared/style-constants.ts index 44c4521..58fa98e 100644 --- a/src/shared/style-constants.ts +++ b/src/shared/style-constants.ts @@ -35,9 +35,15 @@ export const HORIZONTAL_RULE_STYLE_ID = 'HorizontalRule'; // Ditto for a block of preserved raw HTML. export const HTML_PREFORMATTED_STYLE_ID = 'HTMLPreformatted'; +// Ditto for a $$...$$ display-math block (ExaDev/markdown-codec#53) -- preserved raw LaTeX text, not converted to MathML by this package (see src/lower/lower.ts's own top-of-file note). +export const MATH_BLOCK_STYLE_ID = 'MathBlock'; + // A code span/code block's own ContentRun.fontFamily -- a genuinely monospace font every mainstream Word/LibreOffice install carries, matching this whole family's own "standard, not invented" font-naming convention (see documents.js's own standard-14 substitution). export const MONOSPACE_FONT_FAMILY = 'Courier New'; +// An inline math run's own ContentRun.fontFamily marker (ExaDev/markdown-codec#53), the identical opportunistic-reuse trick MONOSPACE_FONT_FAMILY already plays for a code span -- a real Word/LibreOffice math font (the one Word itself uses for OOXML equation runs), not an invented sentinel, so a run genuinely styled this way for an unrelated reason degrades exactly as gracefully as a genuinely-monospace non-code-span run already does. Load-bearing, not cosmetic: escapeMarkdownText (src/emit/inline.ts) backslash-escapes every literal '(' and ')' in ORDINARY text, so an inline math run's own \( \) delimiters cannot be recovered by pattern-matching the written-out text after the fact (that was tried and reverted -- any ordinary parenthetical remark escapes to the identical \(...\) shape and would be misrecognised as math on reparse); this marker is what lets src/emit/inline.ts's own renderLeaf single out a genuine preserved-math run and skip escaping for that one run only. +export const MATH_INLINE_FONT_MARKER = 'Cambria Math'; + // Points per level of blockquote nesting src/lower/src/emit agree on for ContentParagraph.indentLeftPt -- 0.5in, a common real-world blockquote/list indent increment (matching, e.g., Word's own default list-indent step). document-schema.js's own indentLeftPt carries no "this many quote levels" semantic of its own, so SOME fixed per-level unit has to be picked for the two directions to agree; this is that choice, made once, here. export const QUOTE_INDENT_PT = 36; diff --git a/src/test-support/conformance-exclusions.ts b/src/test-support/conformance-exclusions.ts index ecfa12f..515c554 100644 --- a/src/test-support/conformance-exclusions.ts +++ b/src/test-support/conformance-exclusions.ts @@ -15,12 +15,14 @@ const ADJACENT_SAME_DEPTH = 'two independent containers back to back at the same const IMAGE_SRC_UNPRESERVABLE = 'an image with no data: URI destination has no bytes for this test harness to embed (no MarkdownImageResolver was supplied, matching how readMarkdown is actually called here), so it degrades to a hyperlinked text run (MarkdownDiagnosticCodes.IMAGE_UNRESOLVED) -- and even supplying one would not help this specific byte-for-byte comparison, since embedding real bytes re-renders as a data: URI, replacing rather than preserving the original external src the expected HTML still names'; const EMPHASIS_TORTURE = 'several directly-touching nested or sibling emphasis/strong spans (occasionally one crossing a hyperlink\'s own text boundary) leave only CommonMark\'s two delimiter characters to resolve every adjacent boundary at once -- src/emit/inline.ts\'s pickEmphasisMarker resolves the common single-boundary case (intraword adjacency, one sibling touching one wrap) but a genuine three-or-more-way clash has no second fallback character left; a same-kind nesting (emphasis-in-emphasis, strong-in-strong) is additionally flattened outright before this is ever reached (MarkdownDiagnosticCodes.NESTED_EMPHASIS_FLATTENED)'; const RAW_TEXT_TAG_AMBIGUITY = 'literal text that happens to look like an HTML tag (typically from an unescaped backslash inside a malformed link destination, e.g. "") is indistinguishable, once lowered, from genuine preserved raw HTML -- both are just plain ContentRun text with no field recording which one produced it, so escapeMarkdownText\'s own "leave a real tag unescaped" rule (needed for genuine raw HTML to survive) fires on this literal text too'; +const MATH_DELIMITER_DIVERGENCE = 'a source-level \\( directly followed (eventually) by a literal \\) is now read as inline math (ExaDev/markdown-codec#53), a deliberate divergence from cmark\'s own reading of two independently backslash-escaped parentheses -- src/inline/inline.ts\'s own new \\( recognition in parseBackslash cannot distinguish "the author escaped two literal parens" from "the author wrote inline math", because CommonMark\'s grammar gives \\( no third reading to disambiguate against; real Pandoc/GFM math-extension implementations accept the identical trade-off'; export const COMMONMARK_EXCLUSIONS: ReadonlyMap = new Map([ // Tabs [4, MULTI_BLOCK], [5, MULTI_BLOCK], // Backslash escapes + [12, MATH_DELIMITER_DIVERGENCE], [14, SOFT_BREAK], [22, LINK_TITLE], [23, LINK_TITLE],