{"version":3,"sources":["/home/runner/work/tiptap/tiptap/packages/markdown/dist/index.cjs","../src/Extension.ts","../src/MarkdownManager.ts","../src/utils.ts"],"names":[],"mappings":"AAAA;ACAA;AAIE;AACA;AAAA,oCACK;ADDP;AACA;AENA;AAYE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAEF,gCAA4F;AFH5F;AACA;AGRO,SAAS,mBAAA,CAAoB,MAAA,EAAgB,OAAA,EAAiB;AAEnE,EAAA,MAAM,MAAA,EAAQ,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA;AAGhC,EAAA,MAAM,OAAA,EAAS,KAAA,CAEZ,OAAA,CAAQ,CAAA,IAAA,EAAA,GAAQ,CAAC,IAAA,EAAM,EAAE,CAAC,CAAA,CAE1B,GAAA,CAAI,CAAA,IAAA,EAAA,GAAQ,CAAA,EAAA;AAGyB,EAAA;AAC1C;AAUW;AACI,EAAA;AACJ,IAAA;AACT,EAAA;AACc,EAAA;AACL,IAAA;AACT,EAAA;AAE2B,EAAA;AACA,EAAA;AAEQ,EAAA;AAC1B,IAAA;AACT,EAAA;AAEa,EAAA;AACmD,IAAA;AAChE,EAAA;AACF;AAO0F;AACxD,EAAA;AAEkC,EAAA;AACjD,IAAA;AACa,MAAA;AAC1B,MAAA;AACF,IAAA;AAGwC,IAAA;AAC0B,MAAA;AAClE,IAAA;AAEe,IAAA;AACa,MAAA;AAC5B,IAAA;AACD,EAAA;AACM,EAAA;AACT;AAWsC;AACqB,EAAA;AACQ,EAAA;AACpB,IAAA;AAGmB,IAAA;AACnB,MAAA;AAC3C,IAAA;AACD,EAAA;AACM,EAAA;AACT;AAaE;AAEoB,EAAA;AACuC,EAAA;AAIxD,EAAA;AAEkC,EAAA;AACG,EAAA;AACN,IAAA;AAGjB,MAAA;AAEuB,QAAA;AACyB,UAAA;AACzD,QAAA;AACe,QAAA;AACkB,UAAA;AACjC,QAAA;AACD,MAAA;AACwC,IAAA;AAEc,MAAA;AAC3D,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AASU;AACa,EAAA;AAGE,EAAA;AACkB,IAAA;AACc,IAAA;AAChC,IAAA;AACgB,MAAA;AACnC,IAAA;AACD,EAAA;AACe,EAAA;AACX,EAAA;AACT;AASE;AAEoB,EAAA;AAC8C,EAAA;AACd,IAAA;AAChC,IAAA;AACC,MAAA;AACnB,IAAA;AAC8B,IAAA;AAC/B,EAAA;AACM,EAAA;AACT;AAmBE;AACqC,EAAA;AAGiB,EAAA;AAE3C,EAAA;AACuD,IAAA;AAClE,EAAA;AACuC,EAAA;AACzC;AAUe;AAEoB,EAAA;AACxB,IAAA;AACT,EAAA;AAGO,EAAA;AACT;AH3GoE;AACA;AEjFR;AAC7B,EAAA;AAC2B,EAAA;AAC1D;AAE6B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqCxB,EAAA;AAnCsC,IAAA;AAazC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAsD,IAAA;AAGZ,IAAA;AACJ,IAAA;AAEtC;AAAyC,IAAA;AAEzC;AAAsD,IAAA;AAiaQ,IAAA;AA1ehE,IAAA;AAwFmC,IAAA;AACZ,IAAA;AACD,IAAA;AACa,IAAA;AAED,IAAA;AACwB,MAAA;AACtD,IAAA;AAEwB,IAAA;AACQ,IAAA;AAMP,IAAA;AACO,MAAA;AAC6B,MAAA;AACP,MAAA;AACtD,IAAA;AACF,EAAA;AAAA;AAG8B,EAAA;AAChB,IAAA;AACd,EAAA;AAAA;AAG8B,EAAA;AACgB,IAAA;AAC9C,EAAA;AAAA;AAG2B,EAAA;AACyB,IAAA;AACpD,EAAA;AAAA;AAGqB,EAAA;AACL,IAAA;AAChB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOiD,EAAA;AAxInD,IAAA;AA0IkC,IAAA;AAIkC,IAAA;AAEzC,IAAA;AAEX,IAAA;AACa,MAAA;AACzB,IAAA;AAEoC,IAAA;AACoB,MAAA;AACxD,IAAA;AAEG,IAAA;AACC,MAAA;AACA,MAAA;AAC6C,IAAA;AACE,IAAA;AAGC,IAAA;AAGL,IAAA;AAMG,IAAA;AAE9B,IAAA;AACY,IAAA;AAEI,IAAA;AAClC,MAAA;AACU,MAAA;AACV,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AAGgC,IAAA;AACyB,MAAA;AAChC,MAAA;AACmB,MAAA;AAC5C,IAAA;AAGoB,IAAA;AACyC,MAAA;AACnC,MAAA;AACsB,MAAA;AAChD,IAAA;AAGmC,IAAA;AACD,MAAA;AAClC,IAAA;AACF,EAAA;AAE6B,EAAA;AACU,IAAA;AACvC,EAAA;AAEyE,EAAA;AAChE,IAAA;AACgD,MAAA;AACF,MAAA;AACrD,IAAA;AACF,EAAA;AAEqD,EAAA;AAxNvD,IAAA;AAyN0C,IAAA;AACxC,EAAA;AAAA;AAAA;AAAA;AAK8D,EAAA;AACrC,IAAA;AACrB,MAAA;AACF,IAAA;AAEoD,IAAA;AACY,IAAA;AAClB,IAAA;AAE1C,IAAA;AAEQ,IAAA;AACiB,MAAA;AAEa,QAAA;AACZ,QAAA;AACY,UAAA;AAC7B,UAAA;AACT,QAAA;AACO,QAAA;AACT,MAAA;AACK,IAAA;AACgE,MAAA;AACvE,IAAA;AAG4C,IAAA;AAC1C,MAAA;AACA,MAAA;AACO,MAAA;AACqC,MAAA;AAEV,QAAA;AAEW,QAAA;AAEhB,QAAA;AAClB,UAAA;AACF,YAAA;AACkB,YAAA;AACF,YAAA;AACQ,YAAA;AAC7B,UAAA;AACF,QAAA;AAEO,QAAA;AACT,MAAA;AACc,MAAA;AAChB,IAAA;AAGwB,IAAA;AACM,MAAA;AAC7B,IAAA;AACH,EAAA;AAAA;AAGmE,EAAA;AAC7D,IAAA;AACiC,MAAA;AAC7B,IAAA;AACE,MAAA;AACV,IAAA;AACF,EAAA;AAAA;AAG4E,EAAA;AAEpB,IAAA;AACrB,IAAA;AACN,MAAA;AAC3B,IAAA;AAGyD,IAAA;AACE,IAAA;AAC7D,EAAA;AAAA;AAGsE,EAAA;AAChE,IAAA;AACyC,MAAA;AACrC,IAAA;AACE,MAAA;AACV,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAM6C,EAAA;AACxB,IAAA;AACV,MAAA;AACT,IAAA;AAE0D,IAAA;AAEjB,IAAA;AAC3C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOiD,EAAA;AACN,IAAA;AAChC,MAAA;AACT,IAAA;AAKW,IAAA;AAEc,IAAA;AAC3B,EAAA;AAAA;AAAA;AAAA;AAKqC,EAAA;AACZ,IAAA;AACqC,MAAA;AAC5D,IAAA;AAEgC,IAAA;AACI,IAAA;AAEZ,IAAA;AAEpB,IAAA;AAGoC,MAAA;AAGO,MAAA;AAGtC,MAAA;AACC,QAAA;AACN,QAAA;AACF,MAAA;AACA,IAAA;AACwB,MAAA;AAC1B,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAQiB,EAAA;AACuD,IAAA;AACxC,MAAA;AACR,QAAA;AACpB,MAAA;AAEO,MAAA;AACJ,IAAA;AAE4B,IAAA;AACF,IAAA;AAES,IAAA;AAvY5C,MAAA;AAyYwD,MAAA;AAGE,QAAA;AACtB,QAAA;AAC9B,MAAA;AAE4D,MAAA;AACN,QAAA;AAExC,QAAA;AACV,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AAEsC,MAAA;AAEjB,MAAA;AACX,QAAA;AACV,MAAA;AAE+C,MAAA;AAChD,IAAA;AACH,EAAA;AAIE,EAAA;AAG2D,IAAA;AAEjC,IAAA;AAChB,MAAA;AACV,IAAA;AAE6D,IAAA;AACN,IAAA;AAEK,IAAA;AAC9D,EAAA;AAEsD,EAAA;AACK,IAAA;AAC3D,EAAA;AAAA;AAAA;AAAA;AAQsC,EAAA;AACnB,IAAA;AACR,MAAA;AACT,IAAA;AAG2B,IAAA;AACO,MAAA;AAClC,IAAA;AAEoD,IAAA;AACZ,IAAA;AAGA,IAAA;AACV,MAAA;AACnB,QAAA;AACT,MAAA;AAEwD,MAAA;AACA,MAAA;AAGI,MAAA;AAEnC,QAAA;AAChB,QAAA;AACT,MAAA;AAEO,MAAA;AACR,IAAA;AAGmC,IAAA;AACZ,MAAA;AACC,MAAA;AAChB,MAAA;AACT,IAAA;AAGsC,IAAA;AACxC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWiF,EAAA;AACjC,IAAA;AAEJ,MAAA;AAC1C,IAAA;AAE0D,IAAA;AACI,IAAA;AAEN,IAAA;AAEd,MAAA;AAC1C,IAAA;AAQ+F,IAAA;AACpC,IAAA;AACb,IAAA;AAEE,IAAA;AACpB,MAAA;AAC8B,MAAA;AACpC,MAAA;AAER,MAAA;AAE2B,QAAA;AAGT,QAAA;AAGU,QAAA;AACmB,QAAA;AAGpB,QAAA;AACf,QAAA;AAEsB,UAAA;AAGpB,UAAA;AAEa,YAAA;AACqB,YAAA;AACxB,YAAA;AACL,cAAA;AAC2B,gBAAA;AAClD,cAAA;AAE6C,cAAA;AACzB,gBAAA;AACT,kBAAA;AACT,gBAAA;AAC2B,gBAAA;AAC5B,cAAA;AACkD,cAAA;AAEhC,cAAA;AAE2B,gBAAA;AAAiB;AAC/D,cAAA;AACF,YAAA;AACF,UAAA;AACF,QAAA;AAEgB,QAAA;AACR,UAAA;AACD,UAAA;AACL,UAAA;AACA,UAAA;AACoB,UAAA;AACd,UAAA;AACiC,UAAA;AACvC,UAAA;AACF,QAAA;AACF,MAAA;AAE4D,MAAA;AAE9B,MAAA;AACC,QAAA;AAC4B,UAAA;AACzD,QAAA;AAC6B,QAAA;AACf,QAAA;AACT,MAAA;AAC0B,QAAA;AACjC,MAAA;AACF,IAAA;AAE6B,IAAA;AAC4B,MAAA;AACzD,IAAA;AAGgC,IAAA;AACW,IAAA;AACnB,MAAA;AACsC,MAAA;AACrB,MAAA;AAC3B,MAAA;AACiB,QAAA;AACH,UAAA;AACjB,QAAA;AACc,UAAA;AACrB,QAAA;AACF,MAAA;AACF,IAAA;AAEsC,IAAA;AACxC,EAAA;AAAA;AAAA;AAAA;AAKyF,EAAA;AACtE,IAAA;AACR,MAAA;AACT,IAAA;AAEoD,IAAA;AACZ,IAAA;AAGA,IAAA;AACV,MAAA;AACnB,QAAA;AACT,MAAA;AAEwD,MAAA;AACA,MAAA;AAGI,MAAA;AAEnC,QAAA;AAChB,QAAA;AACT,MAAA;AAEO,MAAA;AACR,IAAA;AAGmC,IAAA;AACZ,MAAA;AACC,MAAA;AAChB,MAAA;AACT,IAAA;AAGoC,IAAA;AACtC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMmD,EAAA;AAC1C,IAAA;AACkE,MAAA;AACf,MAAA;AACW,MAAA;AACW,MAAA;AACE,MAAA;AACjE,QAAA;AACL,UAAA;AACN,UAAA;AACgB,UAAA;AAClB,QAAA;AAEO,QAAA;AACT,MAAA;AACoE,MAAA;AACrD,QAAA;AACX,UAAA;AACgB,UAAA;AACI,UAAA;AACtB,QAAA;AAE+C,QAAA;AACjC,UAAA;AACd,QAAA;AAEO,QAAA;AACT,MAAA;AACuE,MAAA;AAC/D,QAAA;AACN,QAAA;AACwD,QAAA;AAC1D,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKyC,EAAA;AACS,IAAA;AAClD,EAAA;AAAA;AAAA;AAAA;AAAA;AAMkE,EAAA;AAtsBpE,IAAA;AAusBmC,IAAA;AAIY,IAAA;AACnB,MAAA;AAEK,MAAA;AAEb,QAAA;AACJ,UAAA;AACmC,UAAA;AAC1C,QAAA;AAC+B,MAAA;AAGA,QAAA;AAGa,QAAA;AACY,QAAA;AAGP,QAAA;AAErB,UAAA;AACoB,UAAA;AACW,UAAA;AACzC,UAAA;AAGW,UAAA;AACmB,UAAA;AAC3B,YAAA;AACO,YAAA;AACV,YAAA;AACmC,YAAA;AACnC,cAAA;AACb,cAAA;AACF,YAAA;AACF,UAAA;AAEuB,UAAA;AAEU,YAAA;AACX,YAAA;AACZ,cAAA;AACD,cAAA;AACC,cAAA;AACC,cAAA;AACT,YAAA;AAE8C,YAAA;AAClC,YAAA;AACgD,cAAA;AAC3B,cAAA;AACJ,gBAAA;AACJ,cAAA;AACC,gBAAA;AACxB,cAAA;AACF,YAAA;AAGI,YAAA;AACJ,YAAA;AACF,UAAA;AACF,QAAA;AAG8C,QAAA;AAC5B,QAAA;AACgD,UAAA;AACjC,UAAA;AACJ,YAAA;AACJ,UAAA;AACC,YAAA;AACxB,UAAA;AACF,QAAA;AACqB,MAAA;AAEiC,QAAA;AACR,QAAA;AACJ,UAAA;AACe,UAAA;AAExB,UAAA;AAEwB,YAAA;AACzB,YAAA;AACvB,UAAA;AAE8C,YAAA;AACpB,YAAA;AACJ,cAAA;AACJ,YAAA;AACC,cAAA;AACxB,YAAA;AACF,UAAA;AACuB,QAAA;AAE4B,UAAA;AACrD,QAAA;AACF,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKiG,EAAA;AACpE,IAAA;AACC,MAAA;AAEa,QAAA;AACuB,QAAA;AACrD,QAAA;AACF,UAAA;AAC8B,UAAA;AACnC,QAAA;AACF,MAAA;AAGO,MAAA;AACF,QAAA;AACuD,QAAA;AAC5D,MAAA;AACD,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAKmE,EAAA;AACR,IAAA;AAC3D,EAAA;AAAA;AAAA;AAAA;AAK8F,EAAA;AAC/E,IAAA;AACJ,MAAA;AACT,IAAA;AAE+B,IAAA;AAEf,MAAA;AAChB,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAQsC,EAAA;AAChB,IAAA;AACb,MAAA;AACI,QAAA;AACC,UAAA;AAC+C,UAAA;AACvD,QAAA;AAEG,MAAA;AACI,QAAA;AACC,UAAA;AAC2B,UAAA;AACoB,UAAA;AACvD,QAAA;AAEG,MAAA;AACI,QAAA;AACC,UAAA;AACmC,UAAA;AAC3C,QAAA;AAEG,MAAA;AAE6B,QAAA;AAE7B,MAAA;AACI,QAAA;AAET,MAAA;AAEoB,QAAA;AACsB,UAAA;AACxC,QAAA;AACO,QAAA;AACX,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYiF,EAAA;AACvC,IAAA;AAEtB,IAAA;AACT,MAAA;AACT,IAAA;AAImC,IAAA;AAEhB,MAAA;AACR,QAAA;AACC,UAAA;AACG,UAAA;AACP,YAAA;AACQ,cAAA;AACA,cAAA;AACR,YAAA;AACF,UAAA;AACF,QAAA;AACF,MAAA;AAEO,MAAA;AACC,QAAA;AACA,QAAA;AACR,MAAA;AACF,IAAA;AAImC,IAAA;AACgB,MAAA;AACnD,IAAA;AAGI,IAAA;AACmD,MAAA;AAGR,MAAA;AAE1B,QAAA;AACD,UAAA;AAChB,QAAA;AAMoB,QAAA;AAGO,UAAA;AAC3B,QAAA;AAEc,QAAA;AAChB,MAAA;AAEO,MAAA;AACO,IAAA;AAC8C,MAAA;AAC9D,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBkD,EAAA;AACG,IAAA;AAE1C,MAAA;AACT,IAAA;AAEgE,IAAA;AACvB,IAAA;AAEd,IAAA;AAClB,MAAA;AACT,IAAA;AAE8C,IAAA;AAEP,IAAA;AACN,MAAA;AACtB,QAAA;AACT,MAAA;AAIuC,MAAA;AAET,MAAA;AAC/B,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAY6C,EAAA;AACT,IAAA;AACpB,MAAA;AACd,IAAA;AAE6B,IAAA;AAEzB,IAAA;AAC0C,MAAA;AAEb,MAAA;AACN,QAAA;AACO,QAAA;AAC5B,UAAA;AACF,QAAA;AACgC,QAAA;AACL,UAAA;AAEuB,YAAA;AACnC,YAAA;AACsB,cAAA;AACjC,YAAA;AACF,UAAA;AACD,QAAA;AACH,MAAA;AAEkE,MAAA;AACA,MAAA;AAC5D,IAAA;AAGR,IAAA;AAE+B,IAAA;AACxB,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAc8F,EAAA;AAGxD,IAAA;AAEzB,IAAA;AACF,MAAA;AACT,IAAA;AAEa,IAAA;AACJ,MAAA;AACC,QAAA;AAC0B,QAAA;AAClC,MAAA;AACF,IAAA;AAE4B,IAAA;AAC9B,EAAA;AAAA;AAAA;AAAA;AAAA;AAMiG,EAAA;AAEhF,IAAA;AAGqC,IAAA;AACtD,EAAA;AAOE,EAAA;AApmCJ,IAAA;AAwmC8B,IAAA;AACiC,MAAA;AAC3D,IAAA;AAEgB,IAAA;AACP,MAAA;AACT,IAAA;AAEiD,IAAA;AACnC,IAAA;AACL,MAAA;AACT,IAAA;AAGgB,IAAA;AACyB,IAAA;AACD,MAAA;AACiB,QAAA;AAEA,QAAA;AACvC,UAAA;AACK,YAAA;AACf,YAAA;AACa,YAAA;AACb,YAAA;AACA,YAAA;AACF,UAAA;AACF,QAAA;AAEsD,QAAA;AACxD,MAAA;AACwC,MAAA;AACe,QAAA;AAEH,QAAA;AACpD,MAAA;AACmB,MAAA;AACU,QAAA;AAC7B,MAAA;AACa,MAAA;AACf,IAAA;AAE+B,IAAA;AAC7B,MAAA;AACA,MAAA;AACwB,MAAA;AACxB,MAAA;AACM,MAAA;AACqB,QAAA;AACtB,QAAA;AACL,MAAA;AACF,IAAA;AAGiB,IAAA;AAEV,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAWE,EAAA;AAGiC,IAAA;AACR,MAAA;AACd,QAAA;AACT,MAAA;AAE0D,MAAA;AAC5D,IAAA;AAEuD,IAAA;AACzD,EAAA;AAAA;AAAA;AAAA;AAAA;AAUE,EAAA;AAE0B,IAAA;AACoB,IAAA;AACG,IAAA;AACa,IAAA;AACnC,IAAA;AAE8B,MAAA;AAEvC,MAAA;AACd,QAAA;AACF,MAAA;AAE0B,MAAA;AACkC,QAAA;AACN,QAAA;AAGI,QAAA;AACI,QAAA;AAaT,QAAA;AACQ,QAAA;AAE5B,QAAA;AAEqB,QAAA;AAEI,UAAA;AAC7B,UAAA;AACyB,YAAA;AACZ,YAAA;AACtC,UAAA;AACF,QAAA;AAEyB,QAAA;AAMA,UAAA;AACa,YAAA;AAC9B,cAAA;AACF,YAAA;AAEsC,YAAA;AACX,YAAA;AACzB,cAAA;AACA,cAAA;AAC6B,cAAA;AAC/B,YAAA;AACmB,YAAA;AACF,cAAA;AACjB,YAAA;AAC+B,YAAA;AACF,cAAA;AACK,cAAA;AAClC,YAAA;AACD,UAAA;AACL,QAAA;AAIwB,QAAA;AACI,QAAA;AACqB,UAAA;AAC7B,UAAA;AACkB,YAAA;AACsB,YAAA;AAC1D,UAAA;AACF,QAAA;AAMwC,QAAA;AACmB,UAAA;AACJ,UAAA;AACnC,UAAA;AACa,YAAA;AAC/B,UAAA;AACsC,UAAA;AACF,UAAA;AACrC,QAAA;AAEwB,QAAA;AAIQ,UAAA;AACD,YAAA;AAC3B,UAAA;AACL,QAAA;AAGkC,QAAA;AAM9B,QAAA;AACoB,QAAA;AACS,UAAA;AAEG,UAAA;AACsB,YAAA;AACnB,cAAA;AACnC,YAAA;AACD,UAAA;AAKmD,UAAA;AAEjD,UAAA;AAGiB,UAAA;AACY,YAAA;AAAA;AAC3B,YAAA;AAAA;AACL,UAAA;AACK,QAAA;AACe,UAAA;AAClB,YAAA;AACA,YAAA;AACA,YAAA;AAC4B,YAAA;AAC9B,UAAA;AACF,QAAA;AAGyB,QAAA;AACS,QAAA;AACgB,UAAA;AAC7B,UAAA;AACmB,YAAA;AACmB,YAAA;AACzD,UAAA;AACF,QAAA;AAEsC,QAAA;AA91C9C,UAAA;AA+1CuB,UAAA;AAC6C,UAAA;AACvC,UAAA;AACF,YAAA;AACjB,UAAA;AAC2B,UAAA;AACK,UAAA;AACjC,QAAA;AAGc,QAAA;AACA,QAAA;AAEQ,QAAA;AAClB,MAAA;AAGiD,QAAA;AAC+B,QAAA;AACnB,QAAA;AAC9B,QAAA;AAn3C5C,UAAA;AAo3CuC,UAAA;AACC,YAAA;AACoB,YAAA;AAClD,UAAA;AACD,QAAA;AAGyD,QAAA;AACb,UAAA;AAC5C,QAAA;AACsB,QAAA;AAG6B,QAAA;AAO9C,QAAA;AAx4Cd,UAAA;AAy4C6D,UAAA;AACH,UAAA;AACY,UAAA;AACvD,QAAA;AAEiD,QAAA;AAC1D,MAAA;AACD,IAAA;AAE2B,IAAA;AAC9B,EAAA;AAAA;AAAA;AAAA;AASU,EAAA;AA55CZ,IAAA;AA65CgC,IAAA;AACnB,MAAA;AACT,IAAA;AAEqD,IAAA;AACD,IAAA;AACX,IAAA;AAChC,MAAA;AACT,IAAA;AAGoB,IAAA;AAGe,IAAA;AAC3B,MAAA;AACgB,MAAA;AACuB,MAAA;AAC/C,IAAA;AAEI,IAAA;AACuB,MAAA;AACvB,QAAA;AACA,QAAA;AACwB,UAAA;AACH,UAAA;AACU,UAAA;AAC8B,UAAA;AAC7D,QAAA;AACmD,QAAA;AACrD,MAAA;AAGqD,MAAA;AACA,MAAA;AACzC,IAAA;AAC8C,MAAA;AAC5D,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AASU,EAAA;AA58CZ,IAAA;AA68CgC,IAAA;AACnB,MAAA;AACT,IAAA;AAEqD,IAAA;AACD,IAAA;AACX,IAAA;AAChC,MAAA;AACT,IAAA;AAGoB,IAAA;AAEe,IAAA;AAC3B,MAAA;AACgB,MAAA;AACuB,MAAA;AAC/C,IAAA;AAEI,IAAA;AACuB,MAAA;AACvB,QAAA;AACA,QAAA;AACwB,UAAA;AACH,UAAA;AACU,UAAA;AAC8B,UAAA;AAC7D,QAAA;AACmD,QAAA;AACrD,MAAA;AAGqD,MAAA;AACC,MAAA;AACJ,MAAA;AACtC,IAAA;AAC8C,MAAA;AAC5D,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAMyF,EAAA;AAClC,IAAA;AACD,IAAA;AAEpC,IAAA;AAClB,EAAA;AAAA;AAAA;AAAA;AAKmF,EAAA;AAChD,IAAA;AACxB,MAAA;AACT,IAAA;AAE4D,IAAA;AACzB,MAAA;AACyB,MAAA;AAC3D,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBE,EAAA;AAE6D,IAAA;AAEhC,IAAA;AACpB,MAAA;AACT,IAAA;AAE4B,IAAA;AAMX,IAAA;AAKsD,IAAA;AAtjD3E,MAAA;AAujDoB,MAAA;AACA,MAAA;AAEO,MAAA;AACJ,QAAA;AACjB,MAAA;AAEkC,MAAA;AACpC,IAAA;AAGmB,IAAA;AAGD,IAAA;AAGkB,IAAA;AACtC,EAAA;AACF;AAEe;AFjWqD;AACA;AChpCyB;AACrF,EAAA;AAEO,EAAA;AACJ,IAAA;AACkC,MAAA;AAC/B,MAAA;AACQ,MAAA;AAClB,IAAA;AACF,EAAA;AAEc,EAAA;AACL,IAAA;AACyD,MAAA;AAEjC,QAAA;AACkB,UAAA;AAC7C,QAAA;AAEqD,QAAA;AAEA,QAAA;AACR,UAAA;AAC7C,QAAA;AAE8D,QAAA;AACjB,QAAA;AAC/C,MAAA;AAEkE,MAAA;AAErC,QAAA;AACmB,UAAA;AAC9C,QAAA;AAEmD,QAAA;AAEE,QAAA;AACP,UAAA;AAC9C,QAAA;AAE4D,QAAA;AACZ,QAAA;AAClD,MAAA;AAEgF,MAAA;AAEnD,QAAA;AAC+B,UAAA;AAC1D,QAAA;AAEmD,QAAA;AAEE,QAAA;AACK,UAAA;AAC1D,QAAA;AAE4D,QAAA;AACA,QAAA;AAC9D,MAAA;AACF,IAAA;AACF,EAAA;AAEa,EAAA;AACJ,IAAA;AACwB,MAAA;AACD,QAAA;AACL,QAAA;AACO,QAAA;AACf,QAAA;AACd,MAAA;AACH,IAAA;AACF,EAAA;AAEiB,EAAA;AAtKnB,IAAA;AAuK8B,IAAA;AAChB,MAAA;AACN,QAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AAE2C,IAAA;AACf,MAAA;AACL,MAAA;AACO,MAAA;AACa,MAAA;AAC1C,IAAA;AAEmC,IAAA;AAGJ,IAAA;AAC6B,MAAA;AAC7D,IAAA;AAEsC,IAAA;AACpC,MAAA;AACF,IAAA;AAEoB,IAAA;AACE,MAAA;AACA,MAAA;AACtB,IAAA;AACgC,IAAA;AAC9B,MAAA;AACF,IAAA;AAE2B,IAAA;AACf,MAAA;AACR,QAAA;AACF,MAAA;AACF,IAAA;AAIc,IAAA;AAEF,MAAA;AACR,QAAA;AACF,MAAA;AACF,IAAA;AAE4D,IAAA;AAKlC,IAAA;AACM,MAAA;AAChC,IAAA;AACF,EAAA;AACD;ADinCmE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/tiptap/tiptap/packages/markdown/dist/index.cjs","sourcesContent":[null,"import {\n  type InsertContentAtOptions as MarkdownInsertContentAtOptions,\n  type InsertContentOptions as MarkdownInsertContentOptions,\n  type SetContentOptions as MarkdownSetContentOptions,\n  commands,\n  Extension,\n} from '@tiptap/core'\nimport type { marked } from 'marked'\n\nimport MarkdownManager from './MarkdownManager.js'\nimport type { ContentType } from './types.js'\nimport { assumeContentType } from './utils.js'\n\ndeclare module '@tiptap/core' {\n  interface Editor {\n    /**\n     * Get the content of the editor as markdown.\n     */\n    getMarkdown: () => string\n\n    /**\n     * The markdown manager instance.\n     */\n    markdown?: MarkdownManager\n  }\n\n  interface EditorOptions {\n    /**\n     * The content type the content is provided as.\n     *\n     * @default 'json'\n     */\n    contentType?: ContentType\n  }\n\n  interface Storage {\n    markdown: MarkdownExtensionStorage\n  }\n\n  interface InsertContentOptions {\n    /**\n     * The content type the content is provided as.\n     *\n     * @default 'json'\n     */\n    contentType?: ContentType\n  }\n\n  interface InsertContentAtOptions {\n    /**\n     * The content type the content is provided as.\n     *\n     * @default 'json'\n     */\n    contentType?: ContentType\n  }\n\n  interface SetContentOptions {\n    /**\n     * The content type the content is provided as.\n     *\n     * @default 'json'\n     */\n    contentType?: ContentType\n  }\n}\n\nexport type MarkdownExtensionOptions = {\n  /**\n   * Configure the indentation style and size for lists and code blocks.\n   * - `style`: Choose between spaces or tabs. Default is 'space'.\n   * - `size`: Number of spaces or tabs for indentation. Default is 2.\n   */\n  indentation?: { style?: 'space' | 'tab'; size?: number }\n\n  /**\n   * Use a custom version of `marked` for markdown parsing and serialization.\n   * If not provided, the default `marked` instance will be used.\n   */\n  marked?: typeof marked\n\n  /**\n   * Options to pass to `marked.setOptions()`.\n   * See the [marked documentation](https://marked.js.org/using_advanced#options) for available options.\n   */\n  markedOptions?: Parameters<typeof marked.setOptions>[0]\n}\n\nexport type MarkdownExtensionStorage = {\n  manager: MarkdownManager\n}\n\nexport const Markdown = Extension.create<MarkdownExtensionOptions, MarkdownExtensionStorage>({\n  name: 'markdown',\n\n  addOptions() {\n    return {\n      indentation: { style: 'space', size: 2 },\n      marked: undefined,\n      markedOptions: {},\n    }\n  },\n\n  addCommands() {\n    return {\n      setContent: (content, options?: MarkdownSetContentOptions) => {\n        // if no contentType is specified, we assume the content is in JSON format OR HTML format\n        if (!options?.contentType) {\n          return commands.setContent(content, options)\n        }\n\n        const actualContentType = assumeContentType(content, options?.contentType)\n\n        if (actualContentType !== 'markdown' || !this.editor.markdown) {\n          return commands.setContent(content, options)\n        }\n\n        const mdContent = this.editor.markdown.parse(content as string)\n        return commands.setContent(mdContent, options)\n      },\n\n      insertContent: (value, options?: MarkdownInsertContentOptions) => {\n        // if no contentType is specified, we assume the content is in JSON format OR HTML format\n        if (!options?.contentType) {\n          return commands.insertContent(value, options)\n        }\n\n        const actualContentType = assumeContentType(value, options?.contentType)\n\n        if (actualContentType !== 'markdown' || !this.editor.markdown) {\n          return commands.insertContent(value, options)\n        }\n\n        const mdContent = this.editor.markdown.parse(value as string)\n        return commands.insertContent(mdContent, options)\n      },\n\n      insertContentAt: (position, value, options?: MarkdownInsertContentAtOptions) => {\n        // if no contentType is specified, we assume the content is in JSON format OR HTML format\n        if (!options?.contentType) {\n          return commands.insertContentAt(position, value, options)\n        }\n\n        const actualContentType = assumeContentType(value, options?.contentType)\n\n        if (actualContentType !== 'markdown' || !this.editor.markdown) {\n          return commands.insertContentAt(position, value, options)\n        }\n\n        const mdContent = this.editor.markdown.parse(value as string)\n        return commands.insertContentAt(position, mdContent, options)\n      },\n    }\n  },\n\n  addStorage() {\n    return {\n      manager: new MarkdownManager({\n        indentation: this.options.indentation,\n        marked: this.options.marked,\n        markedOptions: this.options.markedOptions,\n        extensions: [],\n      }),\n    }\n  },\n\n  onBeforeCreate() {\n    if (this.editor.markdown) {\n      console.error(\n        '[tiptap][markdown]: There is already a `markdown` property on the editor instance. This might lead to unexpected behavior.',\n      )\n      return\n    }\n\n    this.storage.manager = new MarkdownManager({\n      indentation: this.options.indentation,\n      marked: this.options.marked,\n      markedOptions: this.options.markedOptions,\n      extensions: this.editor.extensionManager.baseExtensions,\n    })\n\n    this.editor.markdown = this.storage.manager\n\n    // add a `getMarkdown()` method to the editor\n    this.editor.getMarkdown = () => {\n      return this.storage.manager.serialize(this.editor.getJSON())\n    }\n\n    if (!this.editor.options.contentType) {\n      return\n    }\n\n    const assumedType = assumeContentType(\n      this.editor.options.content,\n      this.editor.options.contentType,\n    )\n    if (assumedType !== 'markdown') {\n      return\n    }\n\n    if (!this.editor.markdown) {\n      throw new Error(\n        '[tiptap][markdown]: The `contentType` option is set to \"markdown\", but the Markdown extension is not added to the editor. Please add the Markdown extension to use this feature.',\n      )\n    }\n\n    if (\n      this.editor.options.content === undefined ||\n      typeof this.editor.options.content !== 'string'\n    ) {\n      throw new Error(\n        '[tiptap][markdown]: The `contentType` option is set to \"markdown\", but the initial content is not a string. Please provide the initial content as a markdown string.',\n      )\n    }\n\n    const json = this.editor.markdown.parse(this.editor.options.content as string)\n\n    // If the parsed markdown produced no content, leave options.content\n    // as-is (empty string) so that createDoc / DOMParser.parse can fill in\n    // the required block node via ProseMirror's ContentMatch.fillBefore.\n    if (json.content?.length) {\n      this.editor.options.content = json\n    }\n  },\n})\n","import {\n  type AnyExtension,\n  type ExtendableConfig,\n  type JSONContent,\n  type MarkdownExtensionSpec,\n  type MarkdownLexerConfiguration,\n  type MarkdownParseHelpers,\n  type MarkdownParseResult,\n  type MarkdownRendererHelpers,\n  type MarkdownToken,\n  type MarkdownTokenizer,\n  type RenderContext,\n  callOrReturn,\n  decodeHtmlEntities,\n  encodeHtmlEntities,\n  flattenExtensions,\n  generateJSON,\n  getExtensionField,\n  getSchema,\n  sortExtensions,\n} from '@tiptap/core'\nimport { type Lexer, type Token, type TokenizerExtension, type TokenizerThis, marked } from 'marked'\n\nimport {\n  attrsEqual,\n  closeMarksBeforeNode,\n  findMarksToClose,\n  findMarksToCloseAtEnd,\n  findMarksToOpen,\n  isTaskItem,\n  reopenMarksAfterNode,\n  wrapInMarkdownBlock,\n} from './utils.js'\n\n/**\n * Returns true when the element's tag is not a recognized standard HTML\n * element. Note: the calling code further cross-references this result\n * against schema parseDOM tags, so non-standard tags that are declared\n * by registered extensions are still treated as valid.\n *\n * Browsers expose this classification natively: any non-hyphenated tag that\n * is not part of the HTML/SVG/MathML namespaces is constructed as an\n * `HTMLUnknownElement`. Standard tags (`<em>`, `<div>`, …) and custom\n * elements (`<my-el>`, must contain a hyphen) are `HTMLElement` instances.\n */\nconst isHtmlUnknownElement = (element: Element): boolean => {\n  const ctor = (window as any).HTMLUnknownElement\n  return typeof ctor === 'function' && element instanceof ctor\n}\n\nexport class MarkdownManager {\n  private markedInstance: typeof marked\n  private activeParseLexer: Lexer | null = null\n  private registry: Map<string, MarkdownExtensionSpec[]>\n  private nodeTypeRegistry: Map<string, MarkdownExtensionSpec[]>\n  /**\n   * Order in which extensions were registered. Used to resolve mark nesting\n   * deterministically when several marks open on the same text node.\n   *\n   * The flattened extensions passed to the manager are pre-sorted by Tiptap's\n   * extension priority (descending), which is also the order ProseMirror uses\n   * to assign mark ranks. Recording that index here lets the serializer place\n   * higher-priority / lower-rank marks (e.g. link with priority 1000) on the\n   * outside without inspecting any rendered markdown output.\n   */\n  private extensionRanks: Map<string, number> = new Map()\n  private indentStyle: 'space' | 'tab'\n  private indentSize: number\n  private baseExtensions: AnyExtension[] = []\n  private extensions: AnyExtension[] = []\n  /** Set of extension names whose `code` spec property is truthy (nodes and marks). */\n  private codeTypes: Set<string> = new Set()\n  /** Lazy cache of tag names declared by the registered schema's parseDOM rules. */\n  private schemaParseDomTagsCache: Set<string> | null = null\n\n  /**\n   * Create a MarkdownManager.\n   * @param options.marked Optional marked instance to use (injected).\n   * @param options.markedOptions Optional options to pass to marked.setOptions\n   * @param options.indentation Indentation settings (style and size).\n   * @param options.extensions An array of Tiptap extensions to register for markdown parsing and rendering.\n   */\n  constructor(options?: {\n    marked?: typeof marked\n    markedOptions?: Parameters<typeof marked.setOptions>[0]\n    indentation?: { style?: 'space' | 'tab'; size?: number }\n    extensions: AnyExtension[]\n  }) {\n    this.markedInstance = options?.marked ?? marked\n    this.indentStyle = options?.indentation?.style ?? 'space'\n    this.indentSize = options?.indentation?.size ?? 2\n    this.baseExtensions = options?.extensions || []\n\n    if (options?.markedOptions && typeof this.markedInstance.setOptions === 'function') {\n      this.markedInstance.setOptions(options.markedOptions)\n    }\n\n    this.registry = new Map()\n    this.nodeTypeRegistry = new Map()\n\n    // If extensions were provided, register them now. Sort by Tiptap priority\n    // first (matching how the editor builds its schema) so the registration\n    // index lines up with ProseMirror's mark rank — this is what the\n    // serializer relies on to nest higher-priority marks like link outermost.\n    if (options?.extensions) {\n      this.baseExtensions = options.extensions\n      const flattened = sortExtensions(flattenExtensions(options.extensions))\n      flattened.forEach(ext => this.registerExtension(ext))\n    }\n  }\n\n  /** Returns the underlying marked instance. */\n  get instance(): typeof marked {\n    return this.markedInstance\n  }\n\n  /** Returns the correct indentCharacter (space or tab) */\n  get indentCharacter(): string {\n    return this.indentStyle === 'space' ? ' ' : '\\t'\n  }\n\n  /** Returns the correct indentString repeated X times */\n  get indentString(): string {\n    return this.indentCharacter.repeat(this.indentSize)\n  }\n\n  /** Helper to quickly check whether a marked instance is available. */\n  hasMarked(): boolean {\n    return !!this.markedInstance\n  }\n\n  /**\n   * Register a Tiptap extension (Node/Mark/Extension). This will read\n   * `markdownName`, `parseMarkdown`, `renderMarkdown` and `priority` from the\n   * extension config (using the same resolution used across the codebase).\n   */\n  registerExtension(extension: AnyExtension): void {\n    // Keep track of all extensions for HTML parsing\n    this.extensions.push(extension)\n\n    // Track extensions that declare `code: true` so we can skip HTML entity\n    // encoding inside code contexts without hardcoding specific type names.\n    const isCode = callOrReturn(getExtensionField(extension, 'code'))\n\n    const name = extension.name\n\n    if (isCode) {\n      this.codeTypes.add(name)\n    }\n\n    if (!this.extensionRanks.has(name)) {\n      this.extensionRanks.set(name, this.extensionRanks.size)\n    }\n    const tokenName =\n      (getExtensionField(\n        extension,\n        'markdownTokenName',\n      ) as ExtendableConfig['markdownTokenName']) || name\n    const parseMarkdown = getExtensionField(extension, 'parseMarkdown') as\n      | ExtendableConfig['parseMarkdown']\n      | undefined\n    const renderMarkdown = getExtensionField(extension, 'renderMarkdown') as\n      | ExtendableConfig['renderMarkdown']\n      | undefined\n    const tokenizer = getExtensionField(extension, 'markdownTokenizer') as\n      | ExtendableConfig['markdownTokenizer']\n      | undefined\n\n    // Read the `markdown` object from the extension config. This allows\n    // extensions to provide `markdown: { name?, parseName?, renderName?, parse?, render?, match? }`.\n    const markdownCfg = (getExtensionField(extension, 'markdownOptions') ??\n      null) as ExtendableConfig['markdownOptions']\n    const isIndenting = markdownCfg?.indentsContent ?? false\n    const htmlReopen = markdownCfg?.htmlReopen\n\n    const spec: MarkdownExtensionSpec = {\n      tokenName,\n      nodeName: name,\n      parseMarkdown,\n      renderMarkdown,\n      isIndenting,\n      htmlReopen,\n      tokenizer,\n    }\n\n    // Add to parse registry using parseName\n    if (tokenName && parseMarkdown) {\n      const parseExisting = this.registry.get(tokenName) || []\n      parseExisting.push(spec)\n      this.registry.set(tokenName, parseExisting)\n    }\n\n    // Add to render registry using renderName (node type)\n    if (renderMarkdown) {\n      const renderExisting = this.nodeTypeRegistry.get(name) || []\n      renderExisting.push(spec)\n      this.nodeTypeRegistry.set(name, renderExisting)\n    }\n\n    // Register custom tokenizer with marked.js\n    if (tokenizer && this.hasMarked()) {\n      this.registerTokenizer(tokenizer)\n    }\n  }\n\n  private createLexer(): Lexer {\n    return new this.markedInstance.Lexer()\n  }\n\n  private createTokenizerHelpers(lexer: Lexer): MarkdownLexerConfiguration {\n    return {\n      inlineTokens: (src: string) => lexer.inlineTokens(src),\n      blockTokens: (src: string) => lexer.blockTokens(src),\n    }\n  }\n\n  private tokenizeInline(src: string): MarkdownToken[] {\n    return (this.activeParseLexer ?? this.createLexer()).inlineTokens(src) as MarkdownToken[]\n  }\n\n  /**\n   * Register a custom tokenizer with marked.js for parsing non-standard markdown syntax.\n   */\n  private registerTokenizer(tokenizer: MarkdownTokenizer): void {\n    if (!this.hasMarked()) {\n      return\n    }\n\n    const { name, start, level = 'inline', tokenize } = tokenizer\n    const createTokenizerHelpers = this.createTokenizerHelpers.bind(this)\n    const createLexer = this.createLexer.bind(this)\n\n    let startCb: (src: string) => number\n\n    if (!start) {\n      startCb = (src: string) => {\n        // For other tokenizers, try to find a match and return its position\n        const result = tokenize(src, [], this.createTokenizerHelpers(this.createLexer()))\n        if (result && result.raw) {\n          const index = src.indexOf(result.raw)\n          return index\n        }\n        return -1\n      }\n    } else {\n      startCb = typeof start === 'function' ? start : (src: string) => src.indexOf(start)\n    }\n\n    // Create marked.js extension with proper types\n    const markedExtension: TokenizerExtension = {\n      name,\n      level,\n      start: startCb,\n      tokenizer(this: TokenizerThis, src, tokens) {\n        const helper = this.lexer\n          ? createTokenizerHelpers(this.lexer)\n          : createTokenizerHelpers(createLexer())\n        const result = tokenize(src, tokens, helper)\n\n        if (result && result.type) {\n          return {\n            ...result,\n            type: result.type || name,\n            raw: result.raw || '',\n            tokens: (result.tokens || []) as Token[],\n          }\n        }\n\n        return undefined\n      },\n      childTokens: [],\n    }\n\n    // Register with marked.js - use extensions array to control priority\n    this.markedInstance.use({\n      extensions: [markedExtension],\n    })\n  }\n\n  /** Get registered handlers for a token type and try each until one succeeds. */\n  private getHandlersForToken(type: string): MarkdownExtensionSpec[] {\n    try {\n      return this.registry.get(type) || []\n    } catch {\n      return []\n    }\n  }\n\n  /** Get the first handler for a token type (for backwards compatibility). */\n  private getHandlerForToken(type: string): MarkdownExtensionSpec | undefined {\n    // First try the markdown token registry (for parsing)\n    const markdownHandlers = this.getHandlersForToken(type)\n    if (markdownHandlers.length > 0) {\n      return markdownHandlers[0]\n    }\n\n    // Then try the node type registry (for rendering)\n    const nodeTypeHandlers = this.getHandlersForNodeType(type)\n    return nodeTypeHandlers.length > 0 ? nodeTypeHandlers[0] : undefined\n  }\n\n  /** Get registered handlers for a node type (for rendering). */\n  private getHandlersForNodeType(type: string): MarkdownExtensionSpec[] {\n    try {\n      return this.nodeTypeRegistry.get(type) || []\n    } catch {\n      return []\n    }\n  }\n\n  /**\n   * Serialize a ProseMirror-like JSON document (or node array) to a Markdown string\n   * using registered renderers and fallback renderers.\n   */\n  serialize(docOrContent: JSONContent): string {\n    if (!docOrContent) {\n      return ''\n    }\n\n    const result = this.renderNodes(docOrContent, docOrContent)\n    // Return empty string if result is only whitespace entities or non-breaking spaces\n    return this.isEmptyOutput(result) ? '' : result\n  }\n\n  /**\n   * Check if the markdown output represents an empty document.\n   * Empty documents may contain only &nbsp; entities or non-breaking space characters\n   * which are used by the Paragraph extension to preserve blank lines.\n   */\n  private isEmptyOutput(markdown: string): boolean {\n    if (!markdown || markdown.trim() === '') {\n      return true\n    }\n\n    // Check if the output is only &nbsp; entities or non-breaking space characters\n    const cleanedOutput = markdown\n      .replace(/&nbsp;/g, '')\n      .replace(/\\u00A0/g, '')\n      .trim()\n    return cleanedOutput === ''\n  }\n\n  /**\n   * Parse markdown string into Tiptap JSON document using registered extension handlers.\n   */\n  parse(markdown: string): JSONContent {\n    if (!this.hasMarked()) {\n      throw new Error('No marked instance available for parsing')\n    }\n\n    const previousParseLexer = this.activeParseLexer\n    const parseLexer = this.createLexer()\n\n    this.activeParseLexer = parseLexer\n\n    try {\n      // Use a parse-scoped lexer so follow-up inline tokenization can reuse\n      // the same configured lexer state without sharing it across parses.\n      const tokens = parseLexer.lex(markdown) as MarkdownToken[]\n\n      // Convert tokens to Tiptap JSON\n      const content = this.parseTokens(tokens, true)\n\n      // Return a document node containing the parsed content\n      return {\n        type: 'doc',\n        content,\n      }\n    } finally {\n      this.activeParseLexer = previousParseLexer\n    }\n  }\n\n  /**\n   * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.\n   */\n  private parseTokens(\n    tokens: MarkdownToken[],\n    parseImplicitEmptyParagraphs = false,\n  ): JSONContent[] {\n    const nonSpaceTokenIndexes = tokens.reduce<number[]>((indexes, token, index) => {\n      if (token.type !== 'space') {\n        indexes.push(index)\n      }\n\n      return indexes\n    }, [])\n\n    let previousNonSpaceTokenIndex = -1\n    let nextNonSpaceTokenPointer = 0\n\n    return tokens.flatMap((token, index) => {\n      while (\n        nextNonSpaceTokenPointer < nonSpaceTokenIndexes.length &&\n        nonSpaceTokenIndexes[nextNonSpaceTokenPointer] < index\n      ) {\n        previousNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer]\n        nextNonSpaceTokenPointer += 1\n      }\n\n      if (parseImplicitEmptyParagraphs && token.type === 'space') {\n        const nextNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer] ?? -1\n\n        return this.createImplicitEmptyParagraphsFromSpace(\n          token,\n          previousNonSpaceTokenIndex,\n          nextNonSpaceTokenIndex,\n        )\n      }\n\n      const parsed = this.parseToken(token, parseImplicitEmptyParagraphs)\n\n      if (parsed === null) {\n        return []\n      }\n\n      return Array.isArray(parsed) ? parsed : [parsed]\n    })\n  }\n\n  private createImplicitEmptyParagraphsFromSpace(\n    token: MarkdownToken,\n    previousNonSpaceTokenIndex: number,\n    nextNonSpaceTokenIndex: number,\n  ): JSONContent[] {\n    const separatorCount = this.countParagraphSeparators(token.raw || '')\n\n    if (separatorCount === 0) {\n      return []\n    }\n\n    const isBoundarySpace = previousNonSpaceTokenIndex === -1 || nextNonSpaceTokenIndex === -1\n    const emptyParagraphCount = Math.max(separatorCount - (isBoundarySpace ? 0 : 1), 0)\n\n    return Array.from({ length: emptyParagraphCount }, () => ({ type: 'paragraph', content: [] }))\n  }\n\n  private countParagraphSeparators(raw: string): number {\n    return (raw.replace(/\\r\\n/g, '\\n').match(/\\n\\n/g) || []).length\n  }\n\n  /**\n   * Parse a single token into Tiptap JSON using the appropriate registered handler.\n   */\n  private parseToken(\n    token: MarkdownToken,\n    parseImplicitEmptyParagraphs = false,\n  ): JSONContent | JSONContent[] | null {\n    if (!token.type) {\n      return null\n    }\n\n    // Special handling for 'list' tokens that may contain mixed bullet/task items\n    if (token.type === 'list') {\n      return this.parseListToken(token)\n    }\n\n    const handlers = this.getHandlersForToken(token.type)\n    const helpers = this.createParseHelpers()\n\n    // Try each handler until one returns a valid result\n    const result = handlers.find(handler => {\n      if (!handler.parseMarkdown) {\n        return false\n      }\n\n      const parseResult = handler.parseMarkdown(token, helpers)\n      const normalized = this.normalizeParseResult(parseResult)\n\n      // Check if this handler returned a valid result (not null/empty array)\n      if (normalized && (!Array.isArray(normalized) || normalized.length > 0)) {\n        // Store result for return\n        this.lastParseResult = normalized\n        return true\n      }\n\n      return false\n    })\n\n    // If a handler worked, return its result\n    if (result && this.lastParseResult) {\n      const toReturn = this.lastParseResult\n      this.lastParseResult = null // Clean up\n      return toReturn\n    }\n\n    // If no handler worked, try fallback parsing\n    return this.parseFallbackToken(token, parseImplicitEmptyParagraphs)\n  }\n\n  private lastParseResult: JSONContent | JSONContent[] | null = null\n\n  /**\n   * Parse a list token, handling mixed bullet and task list items by splitting them into separate lists.\n   * This ensures that consecutive task items and bullet items are grouped and parsed as separate list nodes.\n   *\n   * @param token The list token to parse\n   * @returns Array of parsed list nodes, or null if parsing fails\n   */\n  private parseListToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n    if (!token.items || token.items.length === 0) {\n      // No items, parse normally\n      return this.parseTokenWithHandlers(token)\n    }\n\n    const hasTask = token.items.some(item => isTaskItem(item).isTask)\n    const hasNonTask = token.items.some(item => !isTaskItem(item).isTask)\n\n    if (!hasTask || !hasNonTask || this.getHandlersForToken('taskList').length === 0) {\n      // Not mixed or no taskList extension, parse normally\n      return this.parseTokenWithHandlers(token)\n    }\n\n    // Mixed list with taskList extension available: split into separate lists\n    type TaskListItemToken = MarkdownToken & {\n      type: 'taskItem'\n      checked?: boolean\n      indentLevel?: number\n    }\n    const groups: { type: 'list' | 'taskList'; items: (MarkdownToken | TaskListItemToken)[] }[] = []\n    let currentGroup: (MarkdownToken | TaskListItemToken)[] = []\n    let currentType: 'list' | 'taskList' | null = null\n\n    for (let i = 0; i < token.items.length; i += 1) {\n      const item = token.items[i]\n      const { isTask, checked, indentLevel } = isTaskItem(item)\n      let processedItem = item\n\n      if (isTask) {\n        // Transform list_item into taskItem token\n        const raw = item.raw || item.text || ''\n\n        // Split raw content by lines to separate main content from nested\n        const lines = raw.split('\\n')\n\n        // Extract main content from the first line\n        const firstLineMatch = lines[0].match(/^\\s*[-+*]\\s+\\[([ xX])\\]\\s+(.*)$/)\n        const mainContent = firstLineMatch ? firstLineMatch[2] : ''\n\n        // Parse nested content from remaining lines\n        let nestedTokens: MarkdownToken[] = []\n        if (lines.length > 1) {\n          // Join all lines after the first\n          const nestedRaw = lines.slice(1).join('\\n')\n\n          // Only parse if there's actual content\n          if (nestedRaw.trim()) {\n            // Find minimum indentation of non-empty lines\n            const nestedLines = lines.slice(1)\n            const nonEmptyLines = nestedLines.filter(line => line.trim())\n            if (nonEmptyLines.length > 0) {\n              const minIndent = Math.min(\n                ...nonEmptyLines.map(line => line.length - line.trimStart().length),\n              )\n              // Remove common indentation while preserving structure\n              const trimmedLines = nestedLines.map(line => {\n                if (!line.trim()) {\n                  return '' // Keep empty lines\n                }\n                return line.slice(minIndent)\n              })\n              const nestedContent = trimmedLines.join('\\n').trim()\n              // Use the lexer to parse nested content\n              if (nestedContent) {\n                // Use the full lexer pipeline to ensure inline tokens are populated\n                nestedTokens = this.markedInstance.lexer(`${nestedContent}\\n`)\n              }\n            }\n          }\n        }\n\n        processedItem = {\n          type: 'taskItem',\n          raw: '',\n          mainContent,\n          indentLevel,\n          checked: checked ?? false,\n          text: mainContent,\n          tokens: this.tokenizeInline(mainContent),\n          nestedTokens,\n        }\n      }\n\n      const itemType: 'list' | 'taskList' = isTask ? 'taskList' : 'list'\n\n      if (currentType !== itemType) {\n        if (currentGroup.length > 0) {\n          groups.push({ type: currentType!, items: currentGroup })\n        }\n        currentGroup = [processedItem]\n        currentType = itemType\n      } else {\n        currentGroup.push(processedItem)\n      }\n    }\n\n    if (currentGroup.length > 0) {\n      groups.push({ type: currentType!, items: currentGroup })\n    }\n\n    // Parse each group as a separate token\n    const results: JSONContent[] = []\n    for (let i = 0; i < groups.length; i += 1) {\n      const group = groups[i]\n      const subToken = { ...token, type: group.type, items: group.items }\n      const parsed = this.parseToken(subToken)\n      if (parsed) {\n        if (Array.isArray(parsed)) {\n          results.push(...parsed)\n        } else {\n          results.push(parsed)\n        }\n      }\n    }\n\n    return results.length > 0 ? results : null\n  }\n\n  /**\n   * Parse a token using registered handlers (extracted for reuse).\n   */\n  private parseTokenWithHandlers(token: MarkdownToken): JSONContent | JSONContent[] | null {\n    if (!token.type) {\n      return null\n    }\n\n    const handlers = this.getHandlersForToken(token.type)\n    const helpers = this.createParseHelpers()\n\n    // Try each handler until one returns a valid result\n    const result = handlers.find(handler => {\n      if (!handler.parseMarkdown) {\n        return false\n      }\n\n      const parseResult = handler.parseMarkdown(token, helpers)\n      const normalized = this.normalizeParseResult(parseResult)\n\n      // Check if this handler returned a valid result (not null/empty array)\n      if (normalized && (!Array.isArray(normalized) || normalized.length > 0)) {\n        // Store result for return\n        this.lastParseResult = normalized\n        return true\n      }\n\n      return false\n    })\n\n    // If a handler worked, return its result\n    if (result && this.lastParseResult) {\n      const toReturn = this.lastParseResult\n      this.lastParseResult = null // Clean up\n      return toReturn\n    }\n\n    // If no handler worked, try fallback parsing\n    return this.parseFallbackToken(token)\n  }\n\n  /**\n   * Creates helper functions for parsing markdown tokens.\n   * @returns An object containing helper functions for parsing.\n   */\n  private createParseHelpers(): MarkdownParseHelpers {\n    return {\n      parseInline: (tokens: MarkdownToken[]) => this.parseInlineTokens(tokens),\n      tokenizeInline: (src: string) => this.tokenizeInline(src),\n      parseChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens),\n      parseBlockChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens, true),\n      createTextNode: (text: string, marks?: Array<{ type: string; attrs?: any }>) => {\n        const node = {\n          type: 'text',\n          text,\n          marks: marks || undefined,\n        }\n\n        return node\n      },\n      createNode: (type: string, attrs?: any, content?: JSONContent[]) => {\n        const node = {\n          type,\n          attrs: attrs || undefined,\n          content: content || undefined,\n        }\n\n        if (!attrs || Object.keys(attrs).length === 0) {\n          delete node.attrs\n        }\n\n        return node\n      },\n      applyMark: (markType: string, content: JSONContent[], attrs?: any) => ({\n        mark: markType,\n        content,\n        attrs: attrs && Object.keys(attrs).length > 0 ? attrs : undefined,\n      }),\n    }\n  }\n\n  /**\n   * Escape special regex characters in a string.\n   */\n  private escapeRegex(str: string): string {\n    return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n  }\n\n  /**\n   * Parse inline tokens (bold, italic, links, etc.) into text nodes with marks.\n   * This is the complex part that handles mark nesting and boundaries.\n   */\n  private parseInlineTokens(tokens: MarkdownToken[]): JSONContent[] {\n    const result: JSONContent[] = []\n\n    // Process tokens sequentially using an index so we can lookahead and\n    // merge split inline HTML fragments like: text / <em> / inner / </em> / text\n    for (let i = 0; i < tokens.length; i += 1) {\n      const token = tokens[i]\n\n      if (token.type === 'text') {\n        // Create text node – decode HTML entities so that e.g. `&lt;` displays as `<` in the editor\n        result.push({\n          type: 'text',\n          text: decodeHtmlEntities(token.text || ''),\n        })\n      } else if (token.type === 'html') {\n        // Handle possible split inline HTML by attempting to detect an\n        // opening tag and searching forward for a matching closing tag.\n        const raw = (token.raw ?? token.text ?? '').toString()\n\n        // Quick checks for opening vs. closing tag\n        const isClosing = /^<\\/[\\s]*[\\w-]+/i.test(raw)\n        const openMatch = raw.match(/^<[\\s]*([\\w-]+)(\\s|>|\\/|$)/i)\n\n        // oxlint-disable-next-line prefer-string-starts-ends-with\n        if (!isClosing && openMatch && !/\\/>$/.test(raw)) {\n          // Try to find the corresponding closing html token for this tag\n          const tagName = openMatch[1]\n          const escapedTagName = this.escapeRegex(tagName)\n          const closingRegex = new RegExp(`^<\\\\/\\\\s*${escapedTagName}\\\\b`, 'i')\n          let foundIndex = -1\n\n          // Collect intermediate raw parts to reconstruct full HTML fragment\n          const parts: string[] = [raw]\n          for (let j = i + 1; j < tokens.length; j += 1) {\n            const t = tokens[j]\n            const tRaw = (t.raw ?? t.text ?? '').toString()\n            parts.push(tRaw)\n            if (t.type === 'html' && closingRegex.test(tRaw)) {\n              foundIndex = j\n              break\n            }\n          }\n\n          if (foundIndex !== -1) {\n            // Merge opening + inner + closing into one html fragment and parse\n            const mergedRaw = parts.join('')\n            const mergedToken = {\n              type: 'html',\n              raw: mergedRaw,\n              text: mergedRaw,\n              block: false,\n            } as unknown as MarkdownToken\n\n            const parsed = this.parseHTMLToken(mergedToken)\n            if (parsed) {\n              const normalized = this.normalizeParseResult(parsed as any)\n              if (Array.isArray(normalized)) {\n                result.push(...normalized)\n              } else if (normalized) {\n                result.push(normalized)\n              }\n            }\n\n            // Advance i to the closing token\n            i = foundIndex\n            continue\n          }\n        }\n\n        // Fallback: single html token parse\n        const parsedSingle = this.parseHTMLToken(token)\n        if (parsedSingle) {\n          const normalized = this.normalizeParseResult(parsedSingle as any)\n          if (Array.isArray(normalized)) {\n            result.push(...normalized)\n          } else if (normalized) {\n            result.push(normalized)\n          }\n        }\n      } else if (token.type) {\n        // Handle inline marks (bold, italic, etc.)\n        const markHandler = this.getHandlerForToken(token.type)\n        if (markHandler && markHandler.parseMarkdown) {\n          const helpers = this.createParseHelpers()\n          const parsed = markHandler.parseMarkdown(token, helpers)\n\n          if (this.isMarkResult(parsed)) {\n            // This is a mark result - apply the mark to the content\n            const markedContent = this.applyMarkToContent(parsed.mark, parsed.content, parsed.attrs)\n            result.push(...markedContent)\n          } else {\n            // Regular inline node\n            const normalized = this.normalizeParseResult(parsed)\n            if (Array.isArray(normalized)) {\n              result.push(...normalized)\n            } else if (normalized) {\n              result.push(normalized)\n            }\n          }\n        } else if (token.tokens) {\n          // Fallback: try to parse children if they exist\n          result.push(...this.parseInlineTokens(token.tokens))\n        }\n      }\n    }\n\n    return result\n  }\n\n  /**\n   * Apply a mark to content nodes.\n   */\n  private applyMarkToContent(markType: string, content: JSONContent[], attrs?: any): JSONContent[] {\n    return content.map(node => {\n      if (node.type === 'text') {\n        // Add the mark to existing marks or create new marks array\n        const existingMarks = node.marks || []\n        const newMark = attrs ? { type: markType, attrs } : { type: markType }\n        return {\n          ...node,\n          marks: [...existingMarks, newMark],\n        }\n      }\n\n      // For non-text nodes, recursively apply to content\n      return {\n        ...node,\n        content: node.content ? this.applyMarkToContent(markType, node.content, attrs) : undefined,\n      }\n    })\n  } /**\n   * Check if a parse result represents a mark to be applied.\n   */\n  private isMarkResult(\n    result: any,\n  ): result is { mark: string; content: JSONContent[]; attrs?: any } {\n    return result && typeof result === 'object' && 'mark' in result\n  }\n\n  /**\n   * Normalize parse results to ensure they're valid JSONContent.\n   */\n  private normalizeParseResult(result: MarkdownParseResult): JSONContent | JSONContent[] | null {\n    if (!result) {\n      return null\n    }\n\n    if (this.isMarkResult(result)) {\n      // This shouldn't happen at the top level, but handle it gracefully\n      return result.content\n    }\n\n    return result as JSONContent | JSONContent[]\n  }\n\n  /**\n   * Fallback parsing for common tokens when no specific handler is registered.\n   */\n  private parseFallbackToken(\n    token: MarkdownToken,\n    parseImplicitEmptyParagraphs = false,\n  ): JSONContent | JSONContent[] | null {\n    switch (token.type) {\n      case 'paragraph':\n        return {\n          type: 'paragraph',\n          content: token.tokens ? this.parseInlineTokens(token.tokens) : [],\n        }\n\n      case 'heading':\n        return {\n          type: 'heading',\n          attrs: { level: token.depth || 1 },\n          content: token.tokens ? this.parseInlineTokens(token.tokens) : [],\n        }\n\n      case 'text':\n        return {\n          type: 'text',\n          text: decodeHtmlEntities(token.text || ''),\n        }\n\n      case 'html':\n        // Parse HTML using extensions' parseHTML methods\n        return this.parseHTMLToken(token)\n\n      case 'space':\n        return null\n\n      default:\n        // Unknown token type - try to parse children if they exist\n        if (token.tokens) {\n          return this.parseTokens(token.tokens, parseImplicitEmptyParagraphs)\n        }\n        return null\n    }\n  }\n\n  /**\n   * Parse an HTML token from marked into JSONContent using the registered\n   * extensions' `parseHTML` rules. Falls back to literal text when the HTML\n   * has nothing for the schema to keep.\n   *\n   * @param token Marked HTML token (block or inline).\n   * @example\n   *   parseHTMLToken({ type: 'html', raw: '<em>hi</em>', block: false })\n   *   // → text node with an italic mark\n   */\n  private parseHTMLToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n    const html = token.text || token.raw || ''\n\n    if (!html.trim()) {\n      return null\n    }\n\n    // Check if we're in a server-side environment (no window object)\n    // If so, fall back to treating HTML as plain text to avoid runtime errors\n    if (typeof window === 'undefined') {\n      // For block-level HTML, wrap in a paragraph to maintain valid document structure\n      if (token.block) {\n        return {\n          type: 'paragraph',\n          content: [\n            {\n              type: 'text',\n              text: html,\n            },\n          ],\n        }\n      }\n      // For inline HTML, return plain text\n      return {\n        type: 'text',\n        text: html,\n      }\n    }\n\n    // If the HTML would parse to nothing meaningful, keep the original\n    // characters as literal text instead of dropping them.\n    if (this.isUnrecognizedHtml(html)) {\n      return this.htmlAsLiteralText(html, !!token.block)\n    }\n\n    // Use generateJSON to parse the HTML using extensions' parseHTML rules\n    try {\n      const parsed = generateJSON(html, this.baseExtensions)\n\n      // If the result is a doc node, extract its content\n      if (parsed.type === 'doc' && parsed.content) {\n        // For block-level HTML, return the content array\n        if (token.block) {\n          return parsed.content\n        }\n\n        // For inline HTML, we need to flatten the content appropriately\n        // If there's only one paragraph with content, unwrap it\n        if (\n          parsed.content.length === 1 &&\n          parsed.content[0].type === 'paragraph' &&\n          parsed.content[0].content\n        ) {\n          return parsed.content[0].content\n        }\n\n        return parsed.content\n      }\n\n      return parsed as JSONContent\n    } catch (error) {\n      throw new Error(`Failed to parse HTML in markdown: ${error}`)\n    }\n  }\n\n  /**\n   * Returns true when the HTML contains an element the browser classifies as\n   * `HTMLUnknownElement` – unless a registered extension declares the tag\n   * name in its parseDOM rules, in which case it is treated as a known\n   * custom element.\n   *\n   * Recognized but empty elements such as `<em></em>` or `<span></span>`,\n   * and hyphenated custom elements like `<my-mention>`, are not considered\n   * unrecognized.\n   *\n   * @param html Raw HTML string from a marked token.\n   * @example\n   *   isUnrecognizedHtml('<enter foo bar>')  // → true\n   *   isUnrecognizedHtml('<em></em>')        // → false (empty, but real tag)\n   *   isUnrecognizedHtml('<em>hi</em>')      // → false\n   *   isUnrecognizedHtml('<my-el></my-el>')  // → false (valid custom element)\n   *   isUnrecognizedHtml('<br>')             // → false\n   */\n  private isUnrecognizedHtml(html: string): boolean {\n    if (typeof window === 'undefined' || typeof window.DOMParser === 'undefined') {\n      // Can't reliably detect without DOMParser, so assume it's recognized to avoid false positives\n      return false\n    }\n\n    const dom = new window.DOMParser().parseFromString(`<body>${html}</body>`, 'text/html').body\n    const elements = dom.querySelectorAll('*')\n\n    if (elements.length === 0) {\n      return false\n    }\n\n    const schemaTags = this.getSchemaParseDomTags()\n\n    return Array.from(elements).some(el => {\n      if (!isHtmlUnknownElement(el)) {\n        return false\n      }\n\n      // If the tag is declared by a registered extension's parseDOM rule,\n      // treat it as recognized even though the browser doesn't know it.\n      const tagName = el.tagName.toLowerCase()\n\n      return !schemaTags.has(tagName)\n    })\n  }\n\n  /**\n   * Collect the lower-cased tag names declared by the registered extensions'\n   * parseDOM rules, so custom node/mark elements that use non-hyphenated,\n   * non-standard tag names are treated as recognized HTML. Result is cached for the\n   * lifetime of the manager since extensions don't change after registration.\n   *\n   * @example\n   *   // After registering an extension with parseDOM [{ tag: 'something' }]\n   *   getSchemaParseDomTags().has('something') // → true\n   */\n  private getSchemaParseDomTags(): Set<string> {\n    if (this.schemaParseDomTagsCache) {\n      return this.schemaParseDomTagsCache\n    }\n\n    const tags = new Set<string>()\n\n    try {\n      const schema = getSchema(this.baseExtensions)\n\n      const collect = (spec: any) => {\n        const parseDOM = spec?.parseDOM\n        if (!Array.isArray(parseDOM)) {\n          return\n        }\n        parseDOM.forEach((rule: any) => {\n          if (typeof rule?.tag === 'string') {\n            // Extract the bare tag name from selectors like \"something.example\"\n            const match = rule.tag.match(/^[a-zA-Z][\\w-]*/)\n            if (match) {\n              tags.add(match[0].toLowerCase())\n            }\n          }\n        })\n      }\n\n      Object.values(schema.nodes).forEach(type => collect((type as any).spec))\n      Object.values(schema.marks).forEach(type => collect((type as any).spec))\n    } catch {\n      // If schema construction fails, leave the set empty – detection then\n      // falls back to the HTMLUnknownElement check alone.\n    }\n\n    this.schemaParseDomTagsCache = tags\n    return tags\n  }\n\n  /**\n   * Build a JSONContent that preserves the original HTML markup as literal\n   * text. Used when the HTML would otherwise be silently dropped during\n   * schema-aware parsing.\n   *\n   * @param html Raw HTML string to preserve verbatim.\n   * @param isBlock Whether to wrap the text in a paragraph node (block tokens)\n   *   or return it as a bare text node (inline tokens).\n   * @example\n   *   htmlAsLiteralText('<enter foo>', true)\n   *   // → { type: 'paragraph', content: [{ type: 'text', text: '<enter foo>' }] }\n   */\n  private htmlAsLiteralText(html: string, isBlock: boolean): JSONContent | JSONContent[] | null {\n    // Strip trailing whitespace/newlines that marked appends to block HTML\n    // tokens so the rendered text doesn't end with stray blank lines.\n    const text = html.replace(/\\s+$/, '')\n\n    if (!text) {\n      return null\n    }\n\n    if (isBlock) {\n      return {\n        type: 'paragraph',\n        content: [{ type: 'text', text }],\n      }\n    }\n\n    return { type: 'text', text }\n  }\n\n  /**\n   * Encode HTML entities in text unless the node is inside a code context\n   * (code mark or code-block parent) where literal characters should be preserved.\n   */\n  private encodeTextForMarkdown(text: string, node: JSONContent, parentNode?: JSONContent): string {\n    const isInsideCode =\n      (parentNode?.type != null && this.codeTypes.has(parentNode.type)) ||\n      (node.marks || []).some(m => this.codeTypes.has(typeof m === 'string' ? m : m.type))\n\n    return isInsideCode ? text : encodeHtmlEntities(text)\n  }\n\n  renderNodeToMarkdown(\n    node: JSONContent,\n    parentNode?: JSONContent,\n    index = 0,\n    level = 0,\n    meta: Record<string, any> = {},\n  ): string {\n    // if node is a text node, we simply return it's text content\n    // marks are handled at the array level in renderNodesWithMarkBoundaries\n    if (node.type === 'text') {\n      return this.encodeTextForMarkdown(node.text || '', node, parentNode)\n    }\n\n    if (!node.type) {\n      return ''\n    }\n\n    const handler = this.getHandlerForToken(node.type)\n    if (!handler) {\n      return ''\n    }\n\n    const previousNode =\n      Array.isArray(parentNode?.content) && index > 0 ? parentNode.content[index - 1] : undefined\n    const helpers: MarkdownRendererHelpers = {\n      renderChildren: (nodes, separator) => {\n        const childLevel = handler.isIndenting ? level + 1 : level\n\n        if (!Array.isArray(nodes) && (nodes as any).content) {\n          return this.renderNodes(\n            (nodes as any).content as JSONContent[],\n            node,\n            separator || '',\n            index,\n            childLevel,\n          )\n        }\n\n        return this.renderNodes(nodes, node, separator || '', index, childLevel)\n      },\n      renderChild: (childNode, childIndex) => {\n        const childLevel = handler.isIndenting ? level + 1 : level\n\n        return this.renderNodeToMarkdown(childNode, node, childIndex, childLevel)\n      },\n      indent: content => {\n        return this.indentString + content\n      },\n      wrapInBlock: wrapInMarkdownBlock,\n    }\n\n    const context: RenderContext = {\n      index,\n      level,\n      parentType: parentNode?.type,\n      previousNode,\n      meta: {\n        parentAttrs: parentNode?.attrs,\n        ...meta,\n      },\n    }\n\n    // First render the node itself (this will render children recursively)\n    const rendered = handler.renderMarkdown?.(node, helpers, context) || ''\n\n    return rendered\n  }\n\n  /**\n   * Render a node or an array of nodes. Parent type controls how children\n   * are joined (which determines newline insertion between children).\n   */\n  renderNodes(\n    nodeOrNodes: JSONContent | JSONContent[],\n    parentNode?: JSONContent,\n    separator = '',\n    index = 0,\n    level = 0,\n  ): string {\n    // if we have just one node, call renderNodeToMarkdown directly\n    if (!Array.isArray(nodeOrNodes)) {\n      if (!nodeOrNodes.type) {\n        return ''\n      }\n\n      return this.renderNodeToMarkdown(nodeOrNodes, parentNode, index, level)\n    }\n\n    return this.renderNodesWithMarkBoundaries(nodeOrNodes, parentNode, separator, level)\n  }\n\n  /**\n   * Render an array of nodes while properly tracking mark boundaries.\n   * This handles cases where marks span across multiple text nodes.\n   */\n  private renderNodesWithMarkBoundaries(\n    nodes: JSONContent[],\n    parentNode?: JSONContent,\n    separator = '',\n    level = 0,\n  ): string {\n    const result: string[] = []\n    const activeMarks: Map<string, any> = new Map()\n    const reopenWithHtmlOnNextOpen = new Set<string>()\n    const markOpeningModes = new Map<string, 'markdown' | 'html'>()\n    nodes.forEach((node, i) => {\n      // Lookahead to the next node to determine if marks need to be closed\n      const nextNode = i < nodes.length - 1 ? nodes[i + 1] : null\n\n      if (!node.type) {\n        return\n      }\n\n      if (node.type === 'text') {\n        let textContent = this.encodeTextForMarkdown(node.text || '', node, parentNode)\n        const currentMarks = new Map((node.marks || []).map(mark => [mark.type, mark]))\n\n        // Find marks that need to be closed and opened\n        const marksToOpen = this.getMarksToOpenForSerialization(activeMarks, currentMarks, nextNode)\n        const marksToClose = findMarksToClose(currentMarks, nextNode)\n\n        // When marks simultaneously close (old) AND open (new) at this boundary, the naive\n        // approach of appending old-close and prepending new-open produces interleaved\n        // delimiters like `*456**` (italic open, text, bold close) instead of properly\n        // nested `_456_**` (italic open, text, italic close, bold close).\n        //\n        // The fix: when both are present, defer old mark closings to the end of the node\n        // (after the new marks also close), ensuring correct inner-before-outer order.\n        //\n        // If an already-active mark ends on this node while another mark opens on this same\n        // node, we defer closing the active mark until the end of the node so nesting stays\n        // valid (`**...++abc++**` instead of `**...++abc**++`).\n        const activeMarksClosingHere = marksToClose.filter(markType => activeMarks.has(markType))\n        const hasCrossedBoundary = activeMarksClosingHere.length > 0 && marksToOpen.length > 0\n\n        let middleTrailingWhitespace = ''\n\n        if (marksToClose.length > 0 && !hasCrossedBoundary) {\n          // Extract trailing whitespace before closing marks to prevent invalid markdown like \"**text **\"\n          const middleTrailingMatch = textContent.match(/(\\s+)$/)\n          if (middleTrailingMatch) {\n            middleTrailingWhitespace = middleTrailingMatch[1]\n            textContent = textContent.slice(0, -middleTrailingWhitespace.length)\n          }\n        }\n\n        if (!hasCrossedBoundary) {\n          // Normal path: close marks that are ending here (no new marks opening simultaneously).\n          // Reverse so the last-opened mark closes first (LIFO), preserving valid nesting.\n          marksToClose\n            .slice()\n            .reverse()\n            .forEach(markType => {\n              if (!activeMarks.has(markType)) {\n                return\n              }\n\n              const mark = currentMarks.get(markType)\n              const closeMarkdown = this.getMarkClosing(\n                markType,\n                mark,\n                markOpeningModes.get(markType),\n              )\n              if (closeMarkdown) {\n                textContent += closeMarkdown\n              }\n              if (activeMarks.has(markType)) {\n                activeMarks.delete(markType)\n                markOpeningModes.delete(markType)\n              }\n            })\n        }\n\n        // Open new marks (should be at the beginning)\n        // Extract leading whitespace before opening marks to prevent invalid markdown like \"** text**\"\n        let leadingWhitespace = ''\n        if (marksToOpen.length > 0) {\n          const leadingMatch = textContent.match(/^(\\s+)/)\n          if (leadingMatch) {\n            leadingWhitespace = leadingMatch[1]\n            textContent = textContent.slice(leadingWhitespace.length)\n          }\n        }\n\n        // Snapshot active mark types before opening new marks, so each new mark's delimiter\n        // is chosen based on what is already active (not including itself).\n        // When crossing a boundary, old marks are still in activeMarks here (not yet removed),\n        // so new marks correctly see them as active context.\n        marksToOpen.forEach(({ type, mark }) => {\n          const openingMode = reopenWithHtmlOnNextOpen.has(type) ? 'html' : 'markdown'\n          const openMarkdown = this.getMarkOpening(type, mark, openingMode)\n          if (openMarkdown) {\n            textContent = openMarkdown + textContent\n          }\n          markOpeningModes.set(type, openingMode)\n          reopenWithHtmlOnNextOpen.delete(type)\n        })\n\n        if (!hasCrossedBoundary) {\n          marksToOpen\n            .slice()\n            .reverse()\n            .forEach(({ type, mark }) => {\n              activeMarks.set(type, mark)\n            })\n        }\n\n        // Add leading whitespace before the mark opening\n        textContent = leadingWhitespace + textContent\n\n        // Determine marks to close at the end of this node.\n        // On a crossed boundary, we close new marks (inner) first, then old marks (outer),\n        // ensuring correct nesting order. Both sets are removed from activeMarks so the\n        // next node's marksToOpen will reopen whichever ones continue.\n        let marksToCloseAtEnd: string[]\n        if (hasCrossedBoundary) {\n          const nextMarkTypes = new Set((nextNode?.marks || []).map((mark: any) => mark.type))\n\n          marksToOpen.forEach(({ type }) => {\n            if (nextMarkTypes.has(type) && this.getHtmlReopenTags(type)) {\n              reopenWithHtmlOnNextOpen.add(type)\n            }\n          })\n\n          // Sort the previously-active closures in LIFO order: the mark that\n          // was opened last (innermost) must close first. activeMarks preserves\n          // insertion order, so a higher indexOf means opened later = inner.\n          const activeMarkKeys = Array.from(activeMarks.keys())\n          const activeMarksClosingHereLifo = activeMarksClosingHere\n            .slice()\n            .sort((a, b) => activeMarkKeys.indexOf(b) - activeMarkKeys.indexOf(a))\n\n          marksToCloseAtEnd = [\n            ...marksToOpen.map(m => m.type), // inner (opened here) — close first\n            ...activeMarksClosingHereLifo, // outer (were active before) — close last, LIFO\n          ]\n        } else {\n          marksToCloseAtEnd = findMarksToCloseAtEnd(\n            activeMarks,\n            currentMarks,\n            nextNode,\n            this.markSetsEqual.bind(this),\n          )\n        }\n\n        // Extract trailing whitespace before closing marks to prevent invalid markdown like \"**text **\"\n        let trailingWhitespace = ''\n        if (marksToCloseAtEnd.length > 0) {\n          const trailingMatch = textContent.match(/(\\s+)$/)\n          if (trailingMatch) {\n            trailingWhitespace = trailingMatch[1]\n            textContent = textContent.slice(0, -trailingWhitespace.length)\n          }\n        }\n\n        marksToCloseAtEnd.forEach(markType => {\n          const mark = activeMarks.get(markType) ?? currentMarks.get(markType)\n          const closeMarkdown = this.getMarkClosing(markType, mark, markOpeningModes.get(markType))\n          if (closeMarkdown) {\n            textContent += closeMarkdown\n          }\n          activeMarks.delete(markType)\n          markOpeningModes.delete(markType)\n        })\n\n        // Add trailing whitespace after the mark closing\n        textContent += trailingWhitespace\n        textContent += middleTrailingWhitespace\n\n        result.push(textContent)\n      } else {\n        // For non-text nodes, close all active marks before rendering, then reopen after\n        // Only reopen marks that the node itself carries — marks don't skip over inline atoms.\n        const nodeMarkTypes = new Set((node.marks || []).map((mark: { type: string }) => mark.type))\n        const marksToReopen = new Map<string, { type: string; attrs?: Record<string, any> }>()\n        const openingModesToReopen = new Map<string, 'markdown' | 'html'>()\n        activeMarks.forEach((mark, type) => {\n          if (nodeMarkTypes.has(type)) {\n            marksToReopen.set(type, mark)\n            openingModesToReopen.set(type, markOpeningModes.get(type) ?? 'markdown')\n          }\n        })\n\n        // Close all marks before the node\n        const beforeMarkdown = closeMarksBeforeNode(activeMarks, (markType, mark) => {\n          return this.getMarkClosing(markType, mark, markOpeningModes.get(markType))\n        })\n        markOpeningModes.clear()\n\n        // Render the node\n        const nodeContent = this.renderNodeToMarkdown(node, parentNode, i, level)\n\n        // Reopen marks after the node, but NOT after a hard break\n        // Hard breaks should terminate marks (they create a line break where marks don't continue)\n        const afterMarkdown =\n          node.type === 'hardBreak'\n            ? ''\n            : reopenMarksAfterNode(marksToReopen, activeMarks, (markType, mark) => {\n                const openingMode = openingModesToReopen.get(markType) ?? 'markdown'\n                markOpeningModes.set(markType, openingMode)\n                return this.getMarkOpening(markType, mark, openingMode)\n              })\n\n        result.push(beforeMarkdown + nodeContent + afterMarkdown)\n      }\n    })\n\n    return result.join(separator)\n  }\n\n  /**\n   * Get the opening markdown syntax for a mark type.\n   */\n  private getMarkOpening(\n    markType: string,\n    mark: any,\n    openingMode: 'markdown' | 'html' = 'markdown',\n  ): string {\n    if (openingMode === 'html') {\n      return this.getHtmlReopenTags(markType)?.open || ''\n    }\n\n    const handlers = this.getHandlersForNodeType(markType)\n    const handler = handlers.length > 0 ? handlers[0] : undefined\n    if (!handler || !handler.renderMarkdown) {\n      return ''\n    }\n\n    // Use a unique placeholder that's extremely unlikely to appear in real content\n    const placeholder = '\\uE000__TIPTAP_MARKDOWN_PLACEHOLDER__\\uE001'\n\n    // For most marks, we can extract the opening syntax by rendering a simple case\n    const syntheticNode: JSONContent = {\n      type: markType,\n      attrs: mark.attrs || {},\n      content: [{ type: 'text', text: placeholder }],\n    }\n\n    try {\n      const rendered = handler.renderMarkdown(\n        syntheticNode,\n        {\n          renderChildren: () => placeholder,\n          renderChild: () => placeholder,\n          indent: (content: string) => content,\n          wrapInBlock: (prefix: string, content: string) => prefix + content,\n        },\n        { index: 0, level: 0, parentType: 'text', meta: {} },\n      )\n\n      // Extract the opening part (everything before placeholder)\n      const placeholderIndex = rendered.indexOf(placeholder)\n      return placeholderIndex >= 0 ? rendered.substring(0, placeholderIndex) : ''\n    } catch (err) {\n      throw new Error(`Failed to get mark opening for ${markType}: ${err}`)\n    }\n  }\n\n  /**\n   * Get the closing markdown syntax for a mark type.\n   */\n  private getMarkClosing(\n    markType: string,\n    mark: any,\n    openingMode: 'markdown' | 'html' = 'markdown',\n  ): string {\n    if (openingMode === 'html') {\n      return this.getHtmlReopenTags(markType)?.close || ''\n    }\n\n    const handlers = this.getHandlersForNodeType(markType)\n    const handler = handlers.length > 0 ? handlers[0] : undefined\n    if (!handler || !handler.renderMarkdown) {\n      return ''\n    }\n\n    // Use a unique placeholder that's extremely unlikely to appear in real content\n    const placeholder = '\\uE000__TIPTAP_MARKDOWN_PLACEHOLDER__\\uE001'\n\n    const syntheticNode: JSONContent = {\n      type: markType,\n      attrs: mark.attrs || {},\n      content: [{ type: 'text', text: placeholder }],\n    }\n\n    try {\n      const rendered = handler.renderMarkdown(\n        syntheticNode,\n        {\n          renderChildren: () => placeholder,\n          renderChild: () => placeholder,\n          indent: (content: string) => content,\n          wrapInBlock: (prefix: string, content: string) => prefix + content,\n        },\n        { index: 0, level: 0, parentType: 'text', meta: {} },\n      )\n\n      // Extract the closing part (everything after placeholder)\n      const placeholderIndex = rendered.indexOf(placeholder)\n      const placeholderEnd = placeholderIndex + placeholder.length\n      return placeholderIndex >= 0 ? rendered.substring(placeholderEnd) : ''\n    } catch (err) {\n      throw new Error(`Failed to get mark closing for ${markType}: ${err}`)\n    }\n  }\n\n  /**\n   * Returns the inline HTML tags an extension exposes for overlap-boundary\n   * reopen handling, if that mark explicitly opted into HTML reopen mode.\n   */\n  private getHtmlReopenTags(markType: string): { open: string; close: string } | undefined {\n    const handlers = this.getHandlersForNodeType(markType)\n    const handler = handlers.length > 0 ? handlers[0] : undefined\n\n    return handler?.htmlReopen\n  }\n\n  /**\n   * Check if two mark sets are equal (same types and matching attributes).\n   */\n  private markSetsEqual(marks1: Map<string, any>, marks2: Map<string, any>): boolean {\n    if (marks1.size !== marks2.size) {\n      return false\n    }\n\n    return Array.from(marks1.entries()).every(([type, mark]) => {\n      const otherMark = marks2.get(type)\n      return otherMark && attrsEqual(mark.attrs, otherMark.attrs)\n    })\n  }\n\n  /**\n   * Decide the order in which marks open on the current text node.\n   *\n   * The returned array is iterated head-first when prepending opening\n   * delimiters, so the first entry becomes the innermost mark in the emitted\n   * markdown and the last becomes the outermost. Two stable signals drive\n   * the order — neither one inspects any rendered markdown:\n   *\n   *   1. Marks that end on this node must be inner relative to marks that\n   *      continue into the next node, otherwise the delimiters interleave\n   *      instead of nesting.\n   *   2. Within each lifetime group, marks are sorted so that lower\n   *      registration ranks (i.e. higher Tiptap extension priorities) end up\n   *      outermost. ProseMirror assigns mark ranks in the same priority-aware\n   *      order Tiptap uses when building the schema, so link (priority 1000)\n   *      naturally wraps bold/italic without the serializer needing to peek\n   *      at how any particular mark renders.\n   */\n  private getMarksToOpenForSerialization(\n    activeMarks: Map<string, any>,\n    currentMarks: Map<string, any>,\n    nextNode: any,\n  ) {\n    const marksToOpen = findMarksToOpen(activeMarks, currentMarks)\n\n    if (marksToOpen.length <= 1) {\n      return marksToOpen\n    }\n\n    const nextMarks = nextNode?.marks || []\n\n    // Helper: check if the next node has a mark with the same type AND\n    // matching attributes. Two marks of the same type but with different\n    // attributes are logically distinct and must not be treated as continuing.\n    const continuesInNextNode = (markType: string, attrs: any) =>\n      nextMarks.some((m: any) => m.type === markType && attrsEqual(m.attrs, attrs))\n\n    // Higher rank → earlier in the array → innermost mark. Marks without a\n    // recorded rank fall back to MAX_SAFE_INTEGER so they sort innermost,\n    // matching the implicit \"registered last\" assumption for ad-hoc marks.\n    const byRankInnerFirst = (a: { type: string }, b: { type: string }) => {\n      const rankA = this.extensionRanks.get(a.type) ?? Number.MAX_SAFE_INTEGER\n      const rankB = this.extensionRanks.get(b.type) ?? Number.MAX_SAFE_INTEGER\n\n      if (rankA !== rankB) {\n        return rankB - rankA\n      }\n\n      return a.type.localeCompare(b.type)\n    }\n\n    const endingHere = marksToOpen\n      .filter(mark => !continuesInNextNode(mark.type, mark.mark.attrs))\n      .sort(byRankInnerFirst)\n    const continuing = marksToOpen\n      .filter(mark => continuesInNextNode(mark.type, mark.mark.attrs))\n      .sort(byRankInnerFirst)\n\n    return [...endingHere, ...continuing]\n  }\n}\n\nexport default MarkdownManager\n","import type { Content, MarkdownToken } from '@tiptap/core'\nimport type { Fragment, Node } from '@tiptap/pm/model'\n\nimport type { ContentType } from './types.js'\n\n/**\n * Wraps each line of the content with the given prefix.\n * @param prefix The prefix to wrap each line with.\n * @param content The content to wrap.\n * @returns The content with each line wrapped with the prefix.\n */\nexport function wrapInMarkdownBlock(prefix: string, content: string) {\n  // split content lines\n  const lines = content.split('\\n')\n\n  // add empty strings between every line\n  const output = lines\n    // add empty lines between each block\n    .flatMap(line => [line, ''])\n    // add the prefix to each line\n    .map(line => `${prefix}${line}`)\n    .join('\\n')\n\n  return output.slice(0, output.length - 1)\n}\n\n/**\n * Compare two attribute objects for equality.\n * Handles null/undefined and asserts key presence in both objects so that\n * `{ foo: undefined }` and `{ bar: undefined }` are not treated as equal.\n */\nexport function attrsEqual(\n  a: Record<string, any> | null | undefined,\n  b: Record<string, any> | null | undefined,\n): boolean {\n  if (a === b) {\n    return true\n  }\n  if (!a || !b) {\n    return false\n  }\n\n  const keysA = Object.keys(a)\n  const keysB = Object.keys(b)\n\n  if (keysA.length !== keysB.length) {\n    return false\n  }\n\n  return keysA.every(\n    key => Object.prototype.hasOwnProperty.call(b, key) && Object.is(a[key], b[key]),\n  )\n}\n\n/**\n * Identifies marks that need to be closed, based on the marks in the next node.\n * Compares both mark type and attributes — two marks of the same type with\n * different attributes are treated as distinct and need to be closed/reopened.\n */\nexport function findMarksToClose(currentMarks: Map<string, any>, nextNode: any): string[] {\n  const marksToClose: string[] = []\n\n  Array.from(currentMarks.entries()).forEach(([markType, currentMark]) => {\n    if (!nextNode) {\n      marksToClose.push(markType)\n      return\n    }\n\n    // Check if the next node has a mark of the same type with matching attributes\n    const nextMark = (nextNode.marks || []).find(\n      (mark: any) => mark.type === markType && attrsEqual(mark.attrs, currentMark.attrs),\n    )\n\n    if (!nextMark) {\n      marksToClose.push(markType)\n    }\n  })\n  return marksToClose\n}\n\n/**\n * Identifies marks that need to be opened (in current node but not active, or\n * active with different attributes). Two marks of the same type with different\n * attributes are treated as distinct — the old one must be closed and the new\n * one reopened.\n */\nexport function findMarksToOpen(\n  activeMarks: Map<string, any>,\n  currentMarks: Map<string, any>,\n): Array<{ type: string; mark: any }> {\n  const marksToOpen: Array<{ type: string; mark: any }> = []\n  Array.from(currentMarks.entries()).forEach(([markType, mark]) => {\n    const activeMark = activeMarks.get(markType)\n\n    // Open if the mark type is not active, or if the attributes differ\n    if (!activeMark || !attrsEqual(activeMark.attrs, mark.attrs)) {\n      marksToOpen.push({ type: markType, mark })\n    }\n  })\n  return marksToOpen\n}\n\n/**\n * Determines which marks need to be closed at the end of the current text node.\n * This handles cases where marks end at node boundaries or when transitioning\n * to nodes with different mark sets.\n * Compares both mark type and attributes — two marks of the same type with\n * different attributes are treated as distinct and trigger a close/reopen.\n */\nexport function findMarksToCloseAtEnd(\n  activeMarks: Map<string, any>,\n  currentMarks: Map<string, any>,\n  nextNode: any,\n  markSetsEqual: (a: Map<string, { type: string }>, b: Map<string, { type: string }>) => boolean,\n): string[] {\n  const isLastNode = !nextNode\n  const nextNodeHasNoMarks = nextNode && (!nextNode.marks || nextNode.marks.length === 0)\n  const nextNodeHasDifferentMarks =\n    nextNode &&\n    nextNode.marks &&\n    !markSetsEqual(currentMarks, new Map(nextNode.marks.map((mark: any) => [mark.type, mark])))\n\n  const marksToCloseAtEnd: string[] = []\n  if (isLastNode || nextNodeHasNoMarks || nextNodeHasDifferentMarks) {\n    if (nextNode && nextNode.marks) {\n      Array.from(activeMarks.entries())\n        .reverse()\n        .forEach(([markType, activeMark]) => {\n          // Check if nextNode has a mark of the same type with matching attrs\n          const nextMark = nextNode.marks.find(\n            (m: any) => m.type === markType && attrsEqual(m.attrs, activeMark.attrs),\n          )\n          if (!nextMark) {\n            marksToCloseAtEnd.push(markType)\n          }\n        })\n    } else if (isLastNode || nextNodeHasNoMarks) {\n      // Close all active marks\n      marksToCloseAtEnd.push(...Array.from(activeMarks.keys()).reverse())\n    }\n  }\n\n  return marksToCloseAtEnd\n}\n\n/**\n * Closes active marks before rendering a non-text node.\n * Returns the closing markdown syntax and clears the active marks.\n */\nexport function closeMarksBeforeNode(\n  activeMarks: Map<string, any>,\n  getMarkClosing: (markType: string, mark: any) => string,\n): string {\n  let beforeMarkdown = ''\n  Array.from(activeMarks.keys())\n    .reverse()\n    .forEach(markType => {\n      const mark = activeMarks.get(markType)\n      const closeMarkdown = getMarkClosing(markType, mark)\n      if (closeMarkdown) {\n        beforeMarkdown = closeMarkdown + beforeMarkdown\n      }\n    })\n  activeMarks.clear()\n  return beforeMarkdown\n}\n\n/**\n * Reopens marks after rendering a non-text node.\n * Returns the opening markdown syntax and updates the active marks.\n */\nexport function reopenMarksAfterNode(\n  marksToReopen: Map<string, any>,\n  activeMarks: Map<string, any>,\n  getMarkOpening: (markType: string, mark: any) => string,\n): string {\n  let afterMarkdown = ''\n  Array.from(marksToReopen.entries()).forEach(([markType, mark]) => {\n    const openMarkdown = getMarkOpening(markType, mark)\n    if (openMarkdown) {\n      afterMarkdown += openMarkdown\n    }\n    activeMarks.set(markType, mark)\n  })\n  return afterMarkdown\n}\n\n/**\n * Check if a markdown list item token is a task item and extract its state.\n *\n * @param item The list item token to check\n * @returns Object containing isTask flag, checked state, and indentation level\n *\n * @example\n * ```ts\n * isTaskItem({ raw: '- [ ] Task' }) // { isTask: true, checked: false, indentLevel: 0 }\n * isTaskItem({ raw: '  - [x] Done' }) // { isTask: true, checked: true, indentLevel: 2 }\n * isTaskItem({ raw: '- Regular' }) // { isTask: false, indentLevel: 0 }\n * ```\n */\nexport function isTaskItem(item: MarkdownToken): {\n  isTask: boolean\n  checked?: boolean\n  indentLevel: number\n} {\n  const raw = item.raw || item.text || ''\n\n  // Match patterns like \"- [ ] \" or \"  - [x] \"\n  const match = raw.match(/^(\\s*)[-+*]\\s+\\[([ xX])\\]\\s+/)\n\n  if (match) {\n    return { isTask: true, checked: match[2].toLowerCase() === 'x', indentLevel: match[1].length }\n  }\n  return { isTask: false, indentLevel: 0 }\n}\n\n/**\n * Assumes the content type based off the content.\n * @param content The content to assume the type for.\n * @param contentType The content type that should be prioritized.\n */\nexport function assumeContentType(\n  content: (Content | Fragment | Node) | string,\n  contentType: ContentType,\n): ContentType {\n  // if not a string, we assume it will be a json content object\n  if (typeof content !== 'string') {\n    return 'json'\n  }\n\n  // otherwise we let the content type be what it is\n  return contentType\n}\n"]}