diff --git a/package-lock.json b/package-lock.json index 47ef206f..0b7fdb7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@lezer/highlight": "^1.2.0", "@lezer/markdown": "^1.2.0", "@stackoverflow/stacks-icons": "^6.9.0", - "markdown-it": "^14.0.0", + "markdown-it": "^15.0.3", "orderedmap": "^2.1.1", "prosemirror-commands": "^1.5.2", "prosemirror-highlightjs": "^0.9.1", @@ -20,7 +20,7 @@ "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-lezer": "^0.0.5", - "prosemirror-markdown": "^1.12.0", + "prosemirror-markdown": "https://github.com/StackExchange/prosemirror-markdown/archive/ae92b4af23832ed0fdfe8a3e84651e62ba30301e.tar.gz", "prosemirror-model": "^1.24.1", "prosemirror-schema-basic": "^1.2.2", "prosemirror-schema-list": "^1.3.0", @@ -41,7 +41,6 @@ "@stackoverflow/stacks": "^3.0.0", "@stackoverflow/tsconfig": "^1.0.0", "@types/jest": "^30.0.0", - "@types/markdown-it": "^14.0.0", "@typescript-eslint/eslint-plugin": "^8.71.2", "@typescript-eslint/parser": "^8.71.2", "clean-webpack-plugin": "^4.0.0", @@ -3938,28 +3937,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "license": "MIT" - }, - "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", - "license": "MIT", - "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" - } - }, - "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "license": "MIT" - }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -5067,6 +5044,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/argue-cli": { @@ -6648,6 +6626,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -9510,12 +9489,22 @@ "license": "MIT" }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { - "uc.micro": "^2.0.0" + "uc.micro": "^3.0.0" } }, "node_modules/liquidjs": { @@ -9633,22 +9622,60 @@ "license": "ISC" }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.3.tgz", + "integrity": "sha512-+Az7Ojz3iRGu8KYMvJ6xVhvrxP3eE+vEtJX75Ow88XBpcQNV/0lGX5ZynIIBftREo4DC2jsveAMDaeltmm8QvQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", + "argparse": "^3.0.3", + "entities": "^8.1.0", + "linkify-it": "^6.1.0", + "mdurl": "^2.1.0", "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" + "uc.micro": "^3.0.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.4.tgz", + "integrity": "sha512-fwWK3Zq22l4XJJjx5mcBHGikIk+hJqO+32dPm0Jur7+rtG5x1scOjSfdT9ZKeWHPpsya/l0HHK4tDImBkTrtvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9667,9 +9694,9 @@ "license": "CC0-1.0" }, "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", "license": "MIT" }, "node_modules/media-typer": { @@ -11342,12 +11369,11 @@ }, "node_modules/prosemirror-markdown": { "version": "1.13.1", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz", - "integrity": "sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==", + "resolved": "https://github.com/StackExchange/prosemirror-markdown/archive/ae92b4af23832ed0fdfe8a3e84651e62ba30301e.tar.gz", + "integrity": "sha512-H7SN9pI1Y/CaXqTQ50gOSv9GgR9e85A1TvFxevg7Blvnt6bwRB/upOYDkO4zXNzi/NbvIkyF7I1RQBEcrzNpRA==", "license": "MIT", "dependencies": { - "@types/markdown-it": "^14.0.0", - "markdown-it": "^14.0.0", + "markdown-it": "^15.0.3", "prosemirror-model": "^1.20.0" } }, @@ -13175,9 +13201,9 @@ } }, "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", "license": "MIT" }, "node_modules/uglify-js": { diff --git a/package.json b/package.json index a8aeaeca..d07ffcf3 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "@stackoverflow/stacks": "^3.0.0", "@stackoverflow/tsconfig": "^1.0.0", "@types/jest": "^30.0.0", - "@types/markdown-it": "^14.0.0", "@typescript-eslint/eslint-plugin": "^8.71.2", "@typescript-eslint/parser": "^8.71.2", "clean-webpack-plugin": "^4.0.0", @@ -111,7 +110,7 @@ "@lezer/highlight": "^1.2.0", "@lezer/markdown": "^1.2.0", "@stackoverflow/stacks-icons": "^6.9.0", - "markdown-it": "^14.0.0", + "markdown-it": "^15.0.3", "orderedmap": "^2.1.1", "prosemirror-commands": "^1.5.2", "prosemirror-highlightjs": "^0.9.1", @@ -119,7 +118,7 @@ "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-lezer": "^0.0.5", - "prosemirror-markdown": "^1.12.0", + "prosemirror-markdown": "https://github.com/StackExchange/prosemirror-markdown/archive/ae92b4af23832ed0fdfe8a3e84651e62ba30301e.tar.gz", "prosemirror-model": "^1.24.1", "prosemirror-schema-basic": "^1.2.2", "prosemirror-schema-list": "^1.3.0", diff --git a/plugins/devx/markdown-logging.ts b/plugins/devx/markdown-logging.ts index 45216718..7549c273 100644 --- a/plugins/devx/markdown-logging.ts +++ b/plugins/devx/markdown-logging.ts @@ -1,12 +1,8 @@ import type { EditorPlugin } from "../../src"; import { log } from "../../src/shared/logger"; -import { Ruler } from "markdown-it"; -const ruleNames = (ruler: Ruler): string => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore This is a hack to get at the underbelly of the rule engine - return (ruler.__rules__ as { name: string }[]).map((r) => r.name); -}; +const ruleNames = (ruler: { __rules__: { name: string }[] }): string[] => + ruler.__rules__.map((rule) => rule.name); /** * Adds the ability to configure logging for the parser stages. diff --git a/plugins/official/stack-snippets/src/schema.ts b/plugins/official/stack-snippets/src/schema.ts index eca7ec22..00cec3e7 100644 --- a/plugins/official/stack-snippets/src/schema.ts +++ b/plugins/official/stack-snippets/src/schema.ts @@ -1,5 +1,5 @@ -import MarkdownIt, { Token } from "markdown-it"; -import { MarkdownParser } from "prosemirror-markdown"; +import { MarkdownIt, Token, StateBlock } from "markdown-it"; +import type { MarkdownParser } from "prosemirror-markdown"; import { Utils, MarkdownSerializerNodes } from "../../../../src"; import { assertAttrValue, @@ -61,8 +61,10 @@ export const stackSnippetMarkdownSerializer: MarkdownSerializerNodes = { }, }; -const parseSnippetBlockForMarkdownIt: MarkdownIt.ParserBlock.RuleBlock = ( - state: MarkdownIt.StateBlock, +const parseSnippetBlockForMarkdownIt: Parameters< + MarkdownIt["block"]["ruler"]["push"] +>[1] = ( + state: StateBlock, startLine: number, endLine: number, silent: boolean diff --git a/plugins/sample/japanese-se.ts b/plugins/sample/japanese-se.ts index bb42c50e..b66eddae 100644 --- a/plugins/sample/japanese-se.ts +++ b/plugins/sample/japanese-se.ts @@ -1,4 +1,4 @@ -import MarkdownIt, { ParserInline, StateInline, Token } from "markdown-it"; +import { MarkdownIt, StateInline, Token } from "markdown-it"; import type { EditorPlugin } from "../../src"; // simple proof of concept that adds furigana support from https://japanese.meta.stackexchange.com/questions/806/how-should-i-format-my-questions-on-japanese-language-se/807#807 @@ -115,44 +115,48 @@ function findEndChar( return labelEnd; } -const mdJSEPlugin: ParserInline.RuleInline = function (state, silent) { - const startCharCode = state.src.charCodeAt(state.pos); +const mdJSEPlugin: Parameters[1] = + function (state, silent) { + const startCharCode = state.src.charCodeAt(state.pos); - // quick fail on first character - if (startCharCode !== 0x7b /* { */ && startCharCode !== 0x3010 /* 【 */) { - return false; - } + // quick fail on first character + if ( + startCharCode !== 0x7b /* { */ && + startCharCode !== 0x3010 /* 【 */ + ) { + return false; + } - const endCharCode = - startCharCode === 0x7b /* { */ ? 0x7d /* } */ : 0x3011; /* 】 */ + const endCharCode = + startCharCode === 0x7b /* { */ ? 0x7d /* } */ : 0x3011; /* 】 */ - const endCharPos = findEndChar( - state, - state.pos + 1, - false, - startCharCode, - endCharCode - ); + const endCharPos = findEndChar( + state, + state.pos + 1, + false, + startCharCode, + endCharCode + ); - if (endCharPos < 0) { - return false; - } + if (endCharPos < 0) { + return false; + } - if (!silent) { - const totalContent = state.src.slice(state.pos, endCharPos + 1); - const text = totalContent.slice(1, -1); + if (!silent) { + const totalContent = state.src.slice(state.pos, endCharPos + 1); + const text = totalContent.slice(1, -1); - let token = state.push("jse_furigana_open", "span", 1); - token.attrSet("markup", String.fromCharCode(startCharCode)); - token.content = text; + let token = state.push("jse_furigana_open", "span", 1); + token.attrSet("markup", String.fromCharCode(startCharCode)); + token.content = text; - token = state.push("text", "", 0); - token.content = text; + token = state.push("text", "", 0); + token.content = text; - token = state.push("jse_furigana_close", "span", -1); - token.attrSet("markup", String.fromCharCode(endCharCode)); - } + token = state.push("jse_furigana_close", "span", -1); + token.attrSet("markup", String.fromCharCode(endCharCode)); + } - state.pos = endCharPos + 1; - return true; -}; + state.pos = endCharPos + 1; + return true; + }; diff --git a/src/shared/editor-plugin.ts b/src/shared/editor-plugin.ts index fbdd4319..a0bac06a 100644 --- a/src/shared/editor-plugin.ts +++ b/src/shared/editor-plugin.ts @@ -1,6 +1,6 @@ -import MarkdownIt from "markdown-it"; +import { MarkdownIt } from "markdown-it"; import OrderedMap from "orderedmap"; -import { MarkdownParser } from "prosemirror-markdown"; +import type { MarkdownParser } from "prosemirror-markdown"; import type { MarkSpec, NodeSpec, Schema, SchemaSpec } from "prosemirror-model"; import type { Plugin } from "prosemirror-state"; import { EditorProps } from "prosemirror-view"; diff --git a/src/shared/markdown-it/hardbreak-markup.ts b/src/shared/markdown-it/hardbreak-markup.ts index 9dba5eea..0a7ca9df 100644 --- a/src/shared/markdown-it/hardbreak-markup.ts +++ b/src/shared/markdown-it/hardbreak-markup.ts @@ -1,4 +1,4 @@ -import MarkdownIt from "markdown-it"; +import { MarkdownIt } from "markdown-it"; import { StateCore, Token } from "markdown-it"; function addHardbreakMarkup(tokens: Token[], parent: Token = null) { diff --git a/src/shared/markdown-it/html.ts b/src/shared/markdown-it/html.ts index 0ccf78e1..bc4dbd33 100644 --- a/src/shared/markdown-it/html.ts +++ b/src/shared/markdown-it/html.ts @@ -1,4 +1,4 @@ -import MarkdownIt, { StateCore } from "markdown-it"; +import markdownit, { MarkdownIt, StateCore } from "markdown-it"; import { blockElements, selfClosingElements, @@ -6,7 +6,8 @@ import { TagType, } from "../html-helpers"; -const Token = MarkdownIt().core.State.prototype.Token; +const Token = markdownit.Token; +type Token = InstanceType; interface TagInfo { type: TagType; @@ -126,12 +127,9 @@ function getTagInfo(tag: string): TagInfo { * @param tagInfo The tagInfo to use * @param existing The token to alter; creates a new token if this is empty */ -function tagInfoToToken( - tagInfo: TagInfo, - existing?: MarkdownIt.Token -): MarkdownIt.Token { +function tagInfoToToken(tagInfo: TagInfo, existing?: Token): Token { // if a token was not passed in, create a new empty one - const token: MarkdownIt.Token = existing || new Token("", "", 0); + const token: Token = existing || new Token("", "", 0); // determine the markdown-it Token type for this tag const postfix = tagInfo.isSelfClosing @@ -203,9 +201,7 @@ type parsedBlockTokenInfo = { * @param token The html_block token to parse * @returns The parsed info if able, null if unable */ -function isParseableHtmlBlockToken( - token: MarkdownIt.Token -): parsedBlockTokenInfo { +function isParseableHtmlBlockToken(token: Token): parsedBlockTokenInfo { const content = token.content; // checks if a token matches `content` OR `
` const matches = @@ -261,9 +257,9 @@ function isParseableHtmlBlockToken( * @param parentType The type of the token's parent */ function wrapBareInlineToken( - token: MarkdownIt.Token, + token: Token, parentType: TagType | null -): MarkdownIt.Token[] { +): Token[] { // don't wrap block tokens if (token.block) { return [token]; @@ -291,7 +287,7 @@ function wrapBareInlineToken( * Sanitizes a single token if it is of type `html_inline`; otherwise, its children are sanitized * @param token The token to sanitize `html_inline` tokens in */ -function sanitizeHtmlInlineToken(token: MarkdownIt.Token): MarkdownIt.Token { +function sanitizeHtmlInlineToken(token: Token): Token { let tagInfo: TagInfo = null; if (token.type === "html_inline") { @@ -329,9 +325,7 @@ function sanitizeHtmlInlineToken(token: MarkdownIt.Token): MarkdownIt.Token { * Can return `html_inline` tokens that were unable to be sanitized or have a missing pair * @param tokens The tokens to sanitize */ -function sanitizeInlineHtmlTokens( - tokens: MarkdownIt.Token[] -): MarkdownIt.Token[] { +function sanitizeInlineHtmlTokens(tokens: Token[]): Token[] { tokens = tokens.map(sanitizeHtmlInlineToken).filter((t) => !!t); for (let i = 0, len = tokens.length; i < len; i++) { const openToken = tokens[i]; @@ -442,8 +436,8 @@ function sanitizeHtmlString(content: string) { * Recursively sanitizes all `html_block` tokens by parsing the ones that are able to be simply parsed * @param tokens The tokens to sanitize */ -function sanitizeSimpleHtmlBlockTokens(tokens: MarkdownIt.Token[]) { - const retTokens: MarkdownIt.Token[] = []; +function sanitizeSimpleHtmlBlockTokens(tokens: Token[]) { + const retTokens: Token[] = []; tokens.forEach((token) => { let parsedInfo: parsedBlockTokenInfo = null; @@ -461,12 +455,12 @@ function sanitizeSimpleHtmlBlockTokens(tokens: MarkdownIt.Token[]) { return; } - const newTokens: MarkdownIt.Token[] = []; + const newTokens: Token[] = []; parsedInfo.tagInfo.forEach((tag, i, arr) => { const lastInfo = arr[i - 1] as TagInfo; const isInline = typeof tag === "string" || !tag.isBlock; - let tok: MarkdownIt.Token; + let tok: Token; if (typeof tag === "string") { const t = new Token("text", "", 0); @@ -496,10 +490,8 @@ function sanitizeSimpleHtmlBlockTokens(tokens: MarkdownIt.Token[]) { * Sanitize the content of `html_block` tokens by stripping out all unknown tags * @param tokens The tokens to sanitize */ -function sanitizeBlockHtmlTokens( - tokens: MarkdownIt.Token[] -): MarkdownIt.Token[] { - const retTokens: MarkdownIt.Token[] = []; +function sanitizeBlockHtmlTokens(tokens: Token[]): Token[] { + const retTokens: Token[] = []; tokens.forEach((token) => { if (token.children?.length) { @@ -531,8 +523,8 @@ function sanitizeBlockHtmlTokens( retTokens.push(token); } } else if (token.type === "html_block_container_open") { - const contentOpen = token.attrGet("contentOpen"); - const contentClose = token.attrGet("contentClose"); + const contentOpen = token.attrGet("contentOpen")?.toString(); + const contentClose = token.attrGet("contentClose")?.toString(); token.attrSet("contentOpen", sanitizeHtmlString(contentOpen)); token.attrSet("contentClose", sanitizeHtmlString(contentClose)); @@ -551,10 +543,8 @@ function sanitizeBlockHtmlTokens( * Attempts to merge `html_block` tokens that were split by a newline character * @param tokens The tokens to sanitize */ -function mergeSplitBlockHtmlTokens( - tokens: MarkdownIt.Token[] -): MarkdownIt.Token[] { - const returnTokens: MarkdownIt.Token[] = []; +function mergeSplitBlockHtmlTokens(tokens: Token[]): Token[] { + const returnTokens: Token[] = []; let splitCount = 0; // get all split html_block indexes @@ -590,7 +580,7 @@ function mergeSplitBlockHtmlTokens( blockIndexes = blockIndexes.slice(0, -1); } - let lastOpenToken: MarkdownIt.Token | null = null; + let lastOpenToken: Token | null = null; tokens.forEach((t, i) => { if (t.children && t.children.length) { t.children = mergeSplitBlockHtmlTokens(t.children); diff --git a/src/shared/markdown-it/reference-link.ts b/src/shared/markdown-it/reference-link.ts index 34ff48b0..ea517be7 100644 --- a/src/shared/markdown-it/reference-link.ts +++ b/src/shared/markdown-it/reference-link.ts @@ -1,4 +1,4 @@ -import MarkdownIt, { StateCore, Token } from "markdown-it"; +import markdownit, { MarkdownIt, StateCore, Token } from "markdown-it"; type LinkReference = { title?: string; href?: string }; type LinkReferences = { [key: string]: LinkReference }; @@ -38,7 +38,7 @@ function setLinkReference( label = match[2]; } - const normalizedLabel = new MarkdownIt().utils.normalizeReference( + const normalizedLabel = new markdownit().utils.normalizeReference( label ); reference = references[normalizedLabel]; @@ -67,8 +67,8 @@ function setLinkReference( : "shortcut"; token.markup = "reference"; - token.meta = (token.meta as Record) || {}; - (token.meta as Record)["reference"] = { + token.meta = token.meta || {}; + token.meta["reference"] = { ...reference, label, type: refLinkType, diff --git a/src/shared/markdown-it/spoiler.ts b/src/shared/markdown-it/spoiler.ts index af9e0f2f..2fd34234 100644 --- a/src/shared/markdown-it/spoiler.ts +++ b/src/shared/markdown-it/spoiler.ts @@ -1,5 +1,5 @@ -import MarkdownIt, { StateBlock } from "markdown-it"; -import ParentType = MarkdownIt.StateBlock.ParentType; +import markdownit, { MarkdownIt, StateBlock } from "markdown-it"; +type ParentType = StateBlock["parentType"]; type BlockquoteExtOptions = { followingCharRegex: RegExp; @@ -19,7 +19,7 @@ function blockquoteExt( endLine: number, silent: boolean ): boolean { - const md = new MarkdownIt(); + const md = new markdownit(); const isSpace = (code: number) => md.utils.isSpace(code); // NOTE: we're keeping the source as close to upstream as possible, so ignore errors like this // eslint-disable-next-line no-var diff --git a/src/shared/markdown-it/stack-language-comments.ts b/src/shared/markdown-it/stack-language-comments.ts index de1c4a72..e7165a9e 100644 --- a/src/shared/markdown-it/stack-language-comments.ts +++ b/src/shared/markdown-it/stack-language-comments.ts @@ -1,4 +1,4 @@ -import MarkdownIt, { StateCore, StateBlock } from "markdown-it"; +import { MarkdownIt, StateCore, StateBlock } from "markdown-it"; function langCommentParser( matcher: RegExp, @@ -98,7 +98,7 @@ function sanitizeCodeBlockLangs(state: StateCore) { // TODO we need to support a default language at either parse or render time, will revisit this later in a separate plugin for (const token of state.tokens) { if (token.type === "stack_language_all_comment") { - currentLanguageAll = token.attrGet("language"); + currentLanguageAll = token.attrGet("language")?.toString(); break; } } @@ -113,14 +113,14 @@ function sanitizeCodeBlockLangs(state: StateCore) { currentBlock.type === "stack_language_comment" && nextBlock?.type === "code_block" ) { - const language = currentBlock.attrGet("language"); + const language = currentBlock.attrGet("language")?.toString(); // the "info string" for code fences gets set into Token.info, so reuse that nextBlock.info = language; } // later language-all blocks reset the "default" language for all following code_blocks else if (currentBlock.type === "stack_language_all_comment") { - currentLanguageAll = currentBlock.attrGet("language"); + currentLanguageAll = currentBlock.attrGet("language")?.toString(); } // set the language value on unmarked code blocks if we have a default language set else if ( diff --git a/src/shared/markdown-it/tag-link.ts b/src/shared/markdown-it/tag-link.ts index 70c66790..fb537012 100644 --- a/src/shared/markdown-it/tag-link.ts +++ b/src/shared/markdown-it/tag-link.ts @@ -1,4 +1,4 @@ -import MarkdownIt, { StateInline } from "markdown-it"; +import { MarkdownIt, StateInline } from "markdown-it"; import type { TagLinkOptions } from "../view"; function parse_tag_link( diff --git a/src/shared/markdown-it/tight-list.ts b/src/shared/markdown-it/tight-list.ts index 5a638785..7ca3b775 100644 --- a/src/shared/markdown-it/tight-list.ts +++ b/src/shared/markdown-it/tight-list.ts @@ -1,4 +1,4 @@ -import MarkdownIt, { StateCore, Token } from "markdown-it"; +import { MarkdownIt, StateCore, Token } from "markdown-it"; function tightenList(tokens: Token[]) { let iteratedElements = 0; diff --git a/src/shared/markdown-parser.ts b/src/shared/markdown-parser.ts index 15b26e92..7375adfc 100644 --- a/src/shared/markdown-parser.ts +++ b/src/shared/markdown-parser.ts @@ -1,5 +1,5 @@ -import MarkdownIt, { Token } from "markdown-it"; -import { defaultMarkdownParser, MarkdownParser } from "prosemirror-markdown"; +import markdownit, { MarkdownIt, Token, Env } from "markdown-it"; +import { MarkdownParser, defaultMarkdownParser } from "prosemirror-markdown"; import { NodeType, Schema } from "prosemirror-model"; import { IExternalPluginProvider } from "./editor-plugin"; import { log } from "./logger"; @@ -115,11 +115,14 @@ const customMarkdownParserTokens: MarkdownParser["tokens"] = { node: "image", getAttrs: (tok: Token) => { const attrs: Record = { - src: tok.attrGet("src"), - width: tok.attrGet("width"), - height: tok.attrGet("height"), - alt: tok.attrGet("alt") || tok.children?.[0]?.content || null, - title: tok.attrGet("title"), + src: tok.attrGet("src")?.toString() ?? null, + width: tok.attrGet("width")?.toString() ?? null, + height: tok.attrGet("height")?.toString() ?? null, + alt: + tok.attrGet("alt")?.toString() || + tok.children?.[0]?.content || + null, + title: tok.attrGet("title")?.toString() ?? null, }; if (tok.markup === "reference") { @@ -219,11 +222,6 @@ class SOMarkdownParser extends MarkdownParser { // TODO map the (private?) backing property not exposed by the typings declare schema: Schema; - // TODO the types are wrong on this one... - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - tokens: MarkdownParser["tokens"]; - constructor( schema: Schema, tokenizer: MarkdownIt, @@ -246,15 +244,8 @@ class SOMarkdownParser extends MarkdownParser { * Extended MarkdownIt so we can peek into the tokens during parse * TODO we can likely remove this extended version now that we've moved internal items to plugins */ -class SOMarkdownIt extends MarkdownIt { - constructor( - presetName: MarkdownIt.PresetName, - options?: MarkdownIt.Options - ) { - super(presetName, options); - } - - parse(src: string, env: unknown) { +class SOMarkdownIt extends markdownit { + parse(src: string, env: Env) { const parsed = super.parse(src, env); log("Sanitized markdown token tree", parsed); return parsed; diff --git a/test/shared/markdown-parser.test.ts b/test/shared/markdown-parser.test.ts index fa9b5e68..97827c7d 100644 --- a/test/shared/markdown-parser.test.ts +++ b/test/shared/markdown-parser.test.ts @@ -23,6 +23,40 @@ const markdownParser = buildMarkdownParser( ); describe("SOMarkdownParser", () => { + it("should normalize numeric image attributes from markdown-it plugins", () => { + const parser = buildMarkdownParser( + features, + testRichTextSchema, + externalPluginProvider([ + () => ({ + markdown: { + parser: {}, + serializers: { nodes: {}, marks: {} }, + alterMarkdownIt(md) { + md.core.ruler.push( + "numeric-image-size", + (state) => { + const image = state.tokens[1].children[0]; + image.attrSet("width", 640); + image.attrSet("height", 480); + } + ); + }, + }, + }), + ]) + ); + const image = parser.parse("![alt](https://example.com/image.png)") + .firstChild.firstChild; + expect(image.attrs).toMatchObject({ + src: "https://example.com/image.png", + width: "640", + height: "480", + alt: "alt", + title: null, + }); + }); + describe("html support", () => { it.skip("should support inline html", () => { const doc = markdownParser.parse("test");