From 973e1f2925c74f036955cfaf0bd351ea12aece5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:44:25 +0000 Subject: [PATCH 1/3] build(deps): bump markdown-it from 14.1.0 to 15.0.3 Bumps [markdown-it](https://github.com/markdown-it/markdown-it) from 14.1.0 to 15.0.3. - [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) - [Commits](https://github.com/markdown-it/markdown-it/compare/14.1.0...15.0.3) --- updated-dependencies: - dependency-name: markdown-it dependency-version: 15.0.3 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 138 +++++++++++++++++++++++++++++++++++++++------- package.json | 2 +- 2 files changed, 120 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47ef206f..405f2945 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", @@ -9510,12 +9510,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 +9643,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 +9715,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": { @@ -11351,6 +11399,58 @@ "prosemirror-model": "^1.20.0" } }, + "node_modules/prosemirror-markdown/node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "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" + } + }, + "node_modules/prosemirror-markdown/node_modules/markdown-it": { + "version": "14.3.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.3.tgz", + "integrity": "sha512-L//tDAkjTjJCU6pBUSBTgs3Kg4InVaZuUZog/b6V12UKhcDQ7S78877ibbCanRExwPuYP1CZD7Gd2dxSy1GXRg==", + "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.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/prosemirror-markdown/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==", + "license": "MIT" + }, "node_modules/prosemirror-model": { "version": "1.24.1", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.24.1.tgz", @@ -13175,9 +13275,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..23a39795 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,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", From 6a9431e52c33dc799197d73dce5681e1417f7cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20S=C3=A1nchez=20Blanco?= Date: Tue, 3 Nov 2026 17:46:39 +0100 Subject: [PATCH 2/3] fix(markdown): migrate editor integration to markdown-it v15 --- package-lock.json | 1 - package.json | 1 - plugins/devx/markdown-logging.ts | 8 +-- plugins/official/stack-snippets/src/schema.ts | 10 +-- plugins/sample/japanese-se.ts | 70 ++++++++++--------- src/shared/editor-plugin.ts | 4 +- src/shared/markdown-it/hardbreak-markup.ts | 2 +- src/shared/markdown-it/html.ts | 52 ++++++-------- src/shared/markdown-it/reference-link.ts | 8 +-- src/shared/markdown-it/spoiler.ts | 6 +- .../markdown-it/stack-language-comments.ts | 8 +-- src/shared/markdown-it/tag-link.ts | 2 +- src/shared/markdown-it/tight-list.ts | 2 +- src/shared/markdown-parser-types.ts | 41 +++++++++++ src/shared/markdown-parser.ts | 38 +++++----- test/shared/markdown-parser.test.ts | 34 +++++++++ 16 files changed, 173 insertions(+), 114 deletions(-) create mode 100644 src/shared/markdown-parser-types.ts diff --git a/package-lock.json b/package-lock.json index 405f2945..a22ce240 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 23a39795..0d4c3fba 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", 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..086d2213 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 "../../../../src/shared/markdown-parser-types"; 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..bfccbac9 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 "./markdown-parser-types"; 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-types.ts b/src/shared/markdown-parser-types.ts new file mode 100644 index 00000000..801d2310 --- /dev/null +++ b/src/shared/markdown-parser-types.ts @@ -0,0 +1,41 @@ +import type { MarkdownIt, Token } from "markdown-it"; +import { + MarkdownParser as ProseMirrorMarkdownParser, + defaultMarkdownParser, + ParseSpec, +} from "prosemirror-markdown"; +import type { Attrs, Schema } from "prosemirror-model"; + +/** ProseMirror token handlers using markdown-it's bundled v15 types. */ +export type MarkdownParserTokens = Record< + string, + Omit & { + getAttrs?: ( + token: Token, + tokenStream: Token[], + index: number + ) => Attrs | null; + } +>; + +export type MarkdownParser = Omit< + ProseMirrorMarkdownParser, + "tokenizer" | "tokens" +> & { + readonly tokenizer: MarkdownIt; + readonly tokens: MarkdownParserTokens; +}; + +// ProseMirror still declares markdown-it v14 types. v15 preserves the parser +// runtime API, but allows numeric token attributes and exports different types. +// Keep that upstream declaration mismatch here rather than casting plugin tokens. +export const MarkdownParser = ProseMirrorMarkdownParser as unknown as { + new ( + schema: Schema, + tokenizer: MarkdownIt, + tokens: MarkdownParserTokens + ): MarkdownParser; +}; + +export const defaultMarkdownParserTokens: MarkdownParserTokens = + defaultMarkdownParser.tokens; diff --git a/src/shared/markdown-parser.ts b/src/shared/markdown-parser.ts index 15b26e92..8c3d6788 100644 --- a/src/shared/markdown-parser.ts +++ b/src/shared/markdown-parser.ts @@ -1,5 +1,8 @@ -import MarkdownIt, { Token } from "markdown-it"; -import { defaultMarkdownParser, MarkdownParser } from "prosemirror-markdown"; +import markdownit, { MarkdownIt, Token, Env } from "markdown-it"; +import { + MarkdownParser, + defaultMarkdownParserTokens, +} from "./markdown-parser-types"; import { NodeType, Schema } from "prosemirror-model"; import { IExternalPluginProvider } from "./editor-plugin"; import { log } from "./logger"; @@ -14,7 +17,7 @@ import type { CommonmarkParserFeatures } from "./view"; // extend the default markdown parser's tokens and add our own const customMarkdownParserTokens: MarkdownParser["tokens"] = { - ...defaultMarkdownParser.tokens, + ...defaultMarkdownParserTokens, pre: { block: "pre" }, kbd: { mark: "kbd" }, sup: { mark: "sup" }, @@ -115,11 +118,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 +225,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 +247,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"); From 4659d9274530f2ecd0abfb9be0621089d36a864f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20S=C3=A1nchez=20Blanco?= Date: Wed, 4 Nov 2026 09:22:45 +0100 Subject: [PATCH 3/3] fix(markdown): use native v15 ProseMirror integration --- package-lock.json | 85 ++----------------- package.json | 2 +- plugins/official/stack-snippets/src/schema.ts | 2 +- src/shared/editor-plugin.ts | 2 +- src/shared/markdown-parser-types.ts | 41 --------- src/shared/markdown-parser.ts | 7 +- 6 files changed, 11 insertions(+), 128 deletions(-) delete mode 100644 src/shared/markdown-parser-types.ts diff --git a/package-lock.json b/package-lock.json index a22ce240..0b7fdb7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -3937,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", @@ -5066,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": { @@ -6647,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" @@ -11389,67 +11369,14 @@ }, "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" } }, - "node_modules/prosemirror-markdown/node_modules/linkify-it": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", - "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", - "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" - } - }, - "node_modules/prosemirror-markdown/node_modules/markdown-it": { - "version": "14.3.3", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.3.tgz", - "integrity": "sha512-L//tDAkjTjJCU6pBUSBTgs3Kg4InVaZuUZog/b6V12UKhcDQ7S78877ibbCanRExwPuYP1CZD7Gd2dxSy1GXRg==", - "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.5.0", - "linkify-it": "^5.0.2", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/prosemirror-markdown/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==", - "license": "MIT" - }, "node_modules/prosemirror-model": { "version": "1.24.1", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.24.1.tgz", diff --git a/package.json b/package.json index 0d4c3fba..d07ffcf3 100644 --- a/package.json +++ b/package.json @@ -118,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/official/stack-snippets/src/schema.ts b/plugins/official/stack-snippets/src/schema.ts index 086d2213..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, StateBlock } from "markdown-it"; -import type { MarkdownParser } from "../../../../src/shared/markdown-parser-types"; +import type { MarkdownParser } from "prosemirror-markdown"; import { Utils, MarkdownSerializerNodes } from "../../../../src"; import { assertAttrValue, diff --git a/src/shared/editor-plugin.ts b/src/shared/editor-plugin.ts index bfccbac9..a0bac06a 100644 --- a/src/shared/editor-plugin.ts +++ b/src/shared/editor-plugin.ts @@ -1,6 +1,6 @@ import { MarkdownIt } from "markdown-it"; import OrderedMap from "orderedmap"; -import type { MarkdownParser } from "./markdown-parser-types"; +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-parser-types.ts b/src/shared/markdown-parser-types.ts deleted file mode 100644 index 801d2310..00000000 --- a/src/shared/markdown-parser-types.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { MarkdownIt, Token } from "markdown-it"; -import { - MarkdownParser as ProseMirrorMarkdownParser, - defaultMarkdownParser, - ParseSpec, -} from "prosemirror-markdown"; -import type { Attrs, Schema } from "prosemirror-model"; - -/** ProseMirror token handlers using markdown-it's bundled v15 types. */ -export type MarkdownParserTokens = Record< - string, - Omit & { - getAttrs?: ( - token: Token, - tokenStream: Token[], - index: number - ) => Attrs | null; - } ->; - -export type MarkdownParser = Omit< - ProseMirrorMarkdownParser, - "tokenizer" | "tokens" -> & { - readonly tokenizer: MarkdownIt; - readonly tokens: MarkdownParserTokens; -}; - -// ProseMirror still declares markdown-it v14 types. v15 preserves the parser -// runtime API, but allows numeric token attributes and exports different types. -// Keep that upstream declaration mismatch here rather than casting plugin tokens. -export const MarkdownParser = ProseMirrorMarkdownParser as unknown as { - new ( - schema: Schema, - tokenizer: MarkdownIt, - tokens: MarkdownParserTokens - ): MarkdownParser; -}; - -export const defaultMarkdownParserTokens: MarkdownParserTokens = - defaultMarkdownParser.tokens; diff --git a/src/shared/markdown-parser.ts b/src/shared/markdown-parser.ts index 8c3d6788..7375adfc 100644 --- a/src/shared/markdown-parser.ts +++ b/src/shared/markdown-parser.ts @@ -1,8 +1,5 @@ import markdownit, { MarkdownIt, Token, Env } from "markdown-it"; -import { - MarkdownParser, - defaultMarkdownParserTokens, -} from "./markdown-parser-types"; +import { MarkdownParser, defaultMarkdownParser } from "prosemirror-markdown"; import { NodeType, Schema } from "prosemirror-model"; import { IExternalPluginProvider } from "./editor-plugin"; import { log } from "./logger"; @@ -17,7 +14,7 @@ import type { CommonmarkParserFeatures } from "./view"; // extend the default markdown parser's tokens and add our own const customMarkdownParserTokens: MarkdownParser["tokens"] = { - ...defaultMarkdownParserTokens, + ...defaultMarkdownParser.tokens, pre: { block: "pre" }, kbd: { mark: "kbd" }, sup: { mark: "sup" },