@tiptap/markdown 3.20.1 → 3.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/MarkdownManager.ts +192 -42
- package/src/utils.ts +8 -6
- package/dist/index.cjs +0 -1009
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -283
- package/dist/index.d.ts +0 -283
- package/dist/index.js +0 -1009
- package/dist/index.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/markdown",
|
|
3
3
|
"description": "markdown parser and serializer for tiptap",
|
|
4
|
-
"version": "3.20.
|
|
4
|
+
"version": "3.20.3",
|
|
5
5
|
"homepage": "https://tiptap.dev",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tiptap",
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"marked": "^17.0.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@tiptap/core": "^3.20.
|
|
41
|
-
"@tiptap/pm": "^3.20.
|
|
40
|
+
"@tiptap/core": "^3.20.3",
|
|
41
|
+
"@tiptap/pm": "^3.20.3"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@tiptap/core": "^3.20.
|
|
45
|
-
"@tiptap/pm": "^3.20.
|
|
44
|
+
"@tiptap/core": "^3.20.3",
|
|
45
|
+
"@tiptap/pm": "^3.20.3"
|
|
46
46
|
},
|
|
47
47
|
"repository": {
|
|
48
48
|
"type": "git",
|
package/src/MarkdownManager.ts
CHANGED
|
@@ -114,6 +114,7 @@ export class MarkdownManager {
|
|
|
114
114
|
// extensions to provide `markdown: { name?, parseName?, renderName?, parse?, render?, match? }`.
|
|
115
115
|
const markdownCfg = (getExtensionField(extension, 'markdownOptions') ?? null) as ExtendableConfig['markdownOptions']
|
|
116
116
|
const isIndenting = markdownCfg?.indentsContent ?? false
|
|
117
|
+
const htmlReopen = markdownCfg?.htmlReopen
|
|
117
118
|
|
|
118
119
|
const spec: MarkdownExtensionSpec = {
|
|
119
120
|
tokenName,
|
|
@@ -121,6 +122,7 @@ export class MarkdownManager {
|
|
|
121
122
|
parseMarkdown,
|
|
122
123
|
renderMarkdown,
|
|
123
124
|
isIndenting,
|
|
125
|
+
htmlReopen,
|
|
124
126
|
tokenizer,
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -291,7 +293,7 @@ export class MarkdownManager {
|
|
|
291
293
|
const tokens = this.markedInstance.lexer(markdown)
|
|
292
294
|
|
|
293
295
|
// Convert tokens to Tiptap JSON
|
|
294
|
-
const content = this.parseTokens(tokens)
|
|
296
|
+
const content = this.parseTokens(tokens, true)
|
|
295
297
|
|
|
296
298
|
// Return a document node containing the parsed content
|
|
297
299
|
return {
|
|
@@ -303,17 +305,68 @@ export class MarkdownManager {
|
|
|
303
305
|
/**
|
|
304
306
|
* Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.
|
|
305
307
|
*/
|
|
306
|
-
private parseTokens(tokens: MarkdownToken[]): JSONContent[] {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
private parseTokens(tokens: MarkdownToken[], parseImplicitEmptyParagraphs = false): JSONContent[] {
|
|
309
|
+
const nonSpaceTokenIndexes = tokens.reduce<number[]>((indexes, token, index) => {
|
|
310
|
+
if (token.type !== 'space') {
|
|
311
|
+
indexes.push(index)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return indexes
|
|
315
|
+
}, [])
|
|
316
|
+
|
|
317
|
+
let previousNonSpaceTokenIndex = -1
|
|
318
|
+
let nextNonSpaceTokenPointer = 0
|
|
319
|
+
|
|
320
|
+
return tokens.flatMap((token, index) => {
|
|
321
|
+
while (
|
|
322
|
+
nextNonSpaceTokenPointer < nonSpaceTokenIndexes.length &&
|
|
323
|
+
nonSpaceTokenIndexes[nextNonSpaceTokenPointer] < index
|
|
324
|
+
) {
|
|
325
|
+
previousNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer]
|
|
326
|
+
nextNonSpaceTokenPointer += 1
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (parseImplicitEmptyParagraphs && token.type === 'space') {
|
|
330
|
+
const nextNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer] ?? -1
|
|
331
|
+
|
|
332
|
+
return this.createImplicitEmptyParagraphsFromSpace(token, previousNonSpaceTokenIndex, nextNonSpaceTokenIndex)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const parsed = this.parseToken(token, parseImplicitEmptyParagraphs)
|
|
336
|
+
|
|
337
|
+
if (parsed === null) {
|
|
338
|
+
return []
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return Array.isArray(parsed) ? parsed : [parsed]
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private createImplicitEmptyParagraphsFromSpace(
|
|
346
|
+
token: MarkdownToken,
|
|
347
|
+
previousNonSpaceTokenIndex: number,
|
|
348
|
+
nextNonSpaceTokenIndex: number,
|
|
349
|
+
): JSONContent[] {
|
|
350
|
+
const separatorCount = this.countParagraphSeparators(token.raw || '')
|
|
351
|
+
|
|
352
|
+
if (separatorCount === 0) {
|
|
353
|
+
return []
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const isBoundarySpace = previousNonSpaceTokenIndex === -1 || nextNonSpaceTokenIndex === -1
|
|
357
|
+
const emptyParagraphCount = Math.max(separatorCount - (isBoundarySpace ? 0 : 1), 0)
|
|
358
|
+
|
|
359
|
+
return Array.from({ length: emptyParagraphCount }, () => ({ type: 'paragraph', content: [] }))
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private countParagraphSeparators(raw: string): number {
|
|
363
|
+
return (raw.replace(/\r\n/g, '\n').match(/\n\n/g) || []).length
|
|
311
364
|
}
|
|
312
365
|
|
|
313
366
|
/**
|
|
314
367
|
* Parse a single token into Tiptap JSON using the appropriate registered handler.
|
|
315
368
|
*/
|
|
316
|
-
private parseToken(token: MarkdownToken): JSONContent | JSONContent[] | null {
|
|
369
|
+
private parseToken(token: MarkdownToken, parseImplicitEmptyParagraphs = false): JSONContent | JSONContent[] | null {
|
|
317
370
|
if (!token.type) {
|
|
318
371
|
return null
|
|
319
372
|
}
|
|
@@ -353,7 +406,7 @@ export class MarkdownManager {
|
|
|
353
406
|
}
|
|
354
407
|
|
|
355
408
|
// If no handler worked, try fallback parsing
|
|
356
|
-
return this.parseFallbackToken(token)
|
|
409
|
+
return this.parseFallbackToken(token, parseImplicitEmptyParagraphs)
|
|
357
410
|
}
|
|
358
411
|
|
|
359
412
|
private lastParseResult: JSONContent | JSONContent[] | null = null
|
|
@@ -527,6 +580,7 @@ export class MarkdownManager {
|
|
|
527
580
|
return {
|
|
528
581
|
parseInline: (tokens: MarkdownToken[]) => this.parseInlineTokens(tokens),
|
|
529
582
|
parseChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens),
|
|
583
|
+
parseBlockChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens, true),
|
|
530
584
|
createTextNode: (text: string, marks?: Array<{ type: string; attrs?: any }>) => {
|
|
531
585
|
const node = {
|
|
532
586
|
type: 'text',
|
|
@@ -577,7 +631,6 @@ export class MarkdownManager {
|
|
|
577
631
|
const token = tokens[i]
|
|
578
632
|
|
|
579
633
|
if (token.type === 'text') {
|
|
580
|
-
// Create text node
|
|
581
634
|
result.push({
|
|
582
635
|
type: 'text',
|
|
583
636
|
text: token.text || '',
|
|
@@ -723,7 +776,10 @@ export class MarkdownManager {
|
|
|
723
776
|
/**
|
|
724
777
|
* Fallback parsing for common tokens when no specific handler is registered.
|
|
725
778
|
*/
|
|
726
|
-
private parseFallbackToken(
|
|
779
|
+
private parseFallbackToken(
|
|
780
|
+
token: MarkdownToken,
|
|
781
|
+
parseImplicitEmptyParagraphs = false,
|
|
782
|
+
): JSONContent | JSONContent[] | null {
|
|
727
783
|
switch (token.type) {
|
|
728
784
|
case 'paragraph':
|
|
729
785
|
return {
|
|
@@ -754,7 +810,7 @@ export class MarkdownManager {
|
|
|
754
810
|
default:
|
|
755
811
|
// Unknown token type - try to parse children if they exist
|
|
756
812
|
if (token.tokens) {
|
|
757
|
-
return this.parseTokens(token.tokens)
|
|
813
|
+
return this.parseTokens(token.tokens, parseImplicitEmptyParagraphs)
|
|
758
814
|
}
|
|
759
815
|
return null
|
|
760
816
|
}
|
|
@@ -819,7 +875,13 @@ export class MarkdownManager {
|
|
|
819
875
|
}
|
|
820
876
|
}
|
|
821
877
|
|
|
822
|
-
renderNodeToMarkdown(
|
|
878
|
+
renderNodeToMarkdown(
|
|
879
|
+
node: JSONContent,
|
|
880
|
+
parentNode?: JSONContent,
|
|
881
|
+
index = 0,
|
|
882
|
+
level = 0,
|
|
883
|
+
meta: Record<string, any> = {},
|
|
884
|
+
): string {
|
|
823
885
|
// if node is a text node, we simply return it's text content
|
|
824
886
|
// marks are handled at the array level in renderNodesWithMarkBoundaries
|
|
825
887
|
if (node.type === 'text') {
|
|
@@ -835,6 +897,7 @@ export class MarkdownManager {
|
|
|
835
897
|
return ''
|
|
836
898
|
}
|
|
837
899
|
|
|
900
|
+
const previousNode = Array.isArray(parentNode?.content) && index > 0 ? parentNode.content[index - 1] : undefined
|
|
838
901
|
const helpers: MarkdownRendererHelpers = {
|
|
839
902
|
renderChildren: (nodes, separator) => {
|
|
840
903
|
const childLevel = handler.isIndenting ? level + 1 : level
|
|
@@ -845,6 +908,11 @@ export class MarkdownManager {
|
|
|
845
908
|
|
|
846
909
|
return this.renderNodes(nodes, node, separator || '', index, childLevel)
|
|
847
910
|
},
|
|
911
|
+
renderChild: (childNode, childIndex) => {
|
|
912
|
+
const childLevel = handler.isIndenting ? level + 1 : level
|
|
913
|
+
|
|
914
|
+
return this.renderNodeToMarkdown(childNode, node, childIndex, childLevel)
|
|
915
|
+
},
|
|
848
916
|
indent: content => {
|
|
849
917
|
return this.indentString + content
|
|
850
918
|
},
|
|
@@ -855,8 +923,10 @@ export class MarkdownManager {
|
|
|
855
923
|
index,
|
|
856
924
|
level,
|
|
857
925
|
parentType: parentNode?.type,
|
|
926
|
+
previousNode,
|
|
858
927
|
meta: {
|
|
859
928
|
parentAttrs: parentNode?.attrs,
|
|
929
|
+
...meta,
|
|
860
930
|
},
|
|
861
931
|
}
|
|
862
932
|
|
|
@@ -901,6 +971,8 @@ export class MarkdownManager {
|
|
|
901
971
|
): string {
|
|
902
972
|
const result: string[] = []
|
|
903
973
|
const activeMarks: Map<string, any> = new Map()
|
|
974
|
+
const reopenWithHtmlOnNextOpen = new Set<string>()
|
|
975
|
+
const markOpeningModes = new Map<string, 'markdown' | 'html'>()
|
|
904
976
|
nodes.forEach((node, i) => {
|
|
905
977
|
// Lookahead to the next node to determine if marks need to be closed
|
|
906
978
|
const nextNode = i < nodes.length - 1 ? nodes[i + 1] : null
|
|
@@ -917,9 +989,23 @@ export class MarkdownManager {
|
|
|
917
989
|
const marksToOpen = findMarksToOpen(activeMarks, currentMarks)
|
|
918
990
|
const marksToClose = findMarksToClose(currentMarks, nextNode)
|
|
919
991
|
|
|
992
|
+
// When marks simultaneously close (old) AND open (new) at this boundary, the naive
|
|
993
|
+
// approach of appending old-close and prepending new-open produces interleaved
|
|
994
|
+
// delimiters like `*456**` (italic open, text, bold close) instead of properly
|
|
995
|
+
// nested `_456_**` (italic open, text, italic close, bold close).
|
|
996
|
+
//
|
|
997
|
+
// The fix: when both are present, defer old mark closings to the end of the node
|
|
998
|
+
// (after the new marks also close), ensuring correct inner-before-outer order.
|
|
999
|
+
//
|
|
1000
|
+
// If an already-active mark ends on this node while another mark opens on this same
|
|
1001
|
+
// node, we defer closing the active mark until the end of the node so nesting stays
|
|
1002
|
+
// valid (`**...++abc++**` instead of `**...++abc**++`).
|
|
1003
|
+
const activeMarksClosingHere = marksToClose.filter(markType => activeMarks.has(markType))
|
|
1004
|
+
const hasCrossedBoundary = activeMarksClosingHere.length > 0 && marksToOpen.length > 0
|
|
1005
|
+
|
|
920
1006
|
let middleTrailingWhitespace = ''
|
|
921
1007
|
|
|
922
|
-
if (marksToClose.length > 0) {
|
|
1008
|
+
if (marksToClose.length > 0 && !hasCrossedBoundary) {
|
|
923
1009
|
// Extract trailing whitespace before closing marks to prevent invalid markdown like "**text **"
|
|
924
1010
|
const middleTrailingMatch = textContent.match(/(\s+)$/)
|
|
925
1011
|
if (middleTrailingMatch) {
|
|
@@ -927,18 +1013,25 @@ export class MarkdownManager {
|
|
|
927
1013
|
textContent = textContent.slice(0, -middleTrailingWhitespace.length)
|
|
928
1014
|
}
|
|
929
1015
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1016
|
+
|
|
1017
|
+
if (!hasCrossedBoundary) {
|
|
1018
|
+
// Normal path: close marks that are ending here (no new marks opening simultaneously)
|
|
1019
|
+
marksToClose.forEach(markType => {
|
|
1020
|
+
if (!activeMarks.has(markType)) {
|
|
1021
|
+
return
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const mark = currentMarks.get(markType)
|
|
1025
|
+
const closeMarkdown = this.getMarkClosing(markType, mark, markOpeningModes.get(markType))
|
|
1026
|
+
if (closeMarkdown) {
|
|
1027
|
+
textContent += closeMarkdown
|
|
1028
|
+
}
|
|
1029
|
+
if (activeMarks.has(markType)) {
|
|
1030
|
+
activeMarks.delete(markType)
|
|
1031
|
+
markOpeningModes.delete(markType)
|
|
1032
|
+
}
|
|
1033
|
+
})
|
|
1034
|
+
}
|
|
942
1035
|
|
|
943
1036
|
// Open new marks (should be at the beginning)
|
|
944
1037
|
// Extract leading whitespace before opening marks to prevent invalid markdown like "** text**"
|
|
@@ -951,26 +1044,53 @@ export class MarkdownManager {
|
|
|
951
1044
|
}
|
|
952
1045
|
}
|
|
953
1046
|
|
|
1047
|
+
// Snapshot active mark types before opening new marks, so each new mark's delimiter
|
|
1048
|
+
// is chosen based on what is already active (not including itself).
|
|
1049
|
+
// When crossing a boundary, old marks are still in activeMarks here (not yet removed),
|
|
1050
|
+
// so new marks correctly see them as active context.
|
|
954
1051
|
marksToOpen.forEach(({ type, mark }) => {
|
|
955
|
-
const
|
|
1052
|
+
const openingMode = reopenWithHtmlOnNextOpen.has(type) ? 'html' : 'markdown'
|
|
1053
|
+
const openMarkdown = this.getMarkOpening(type, mark, openingMode)
|
|
956
1054
|
if (openMarkdown) {
|
|
957
1055
|
textContent = openMarkdown + textContent
|
|
958
1056
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
}
|
|
1057
|
+
markOpeningModes.set(type, openingMode)
|
|
1058
|
+
reopenWithHtmlOnNextOpen.delete(type)
|
|
962
1059
|
})
|
|
963
1060
|
|
|
1061
|
+
if (!hasCrossedBoundary) {
|
|
1062
|
+
marksToOpen
|
|
1063
|
+
.slice()
|
|
1064
|
+
.reverse()
|
|
1065
|
+
.forEach(({ type, mark }) => {
|
|
1066
|
+
activeMarks.set(type, mark)
|
|
1067
|
+
})
|
|
1068
|
+
}
|
|
1069
|
+
|
|
964
1070
|
// Add leading whitespace before the mark opening
|
|
965
1071
|
textContent = leadingWhitespace + textContent
|
|
966
1072
|
|
|
967
|
-
//
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1073
|
+
// Determine marks to close at the end of this node.
|
|
1074
|
+
// On a crossed boundary, we close new marks (inner) first, then old marks (outer),
|
|
1075
|
+
// ensuring correct nesting order. Both sets are removed from activeMarks so the
|
|
1076
|
+
// next node's marksToOpen will reopen whichever ones continue.
|
|
1077
|
+
let marksToCloseAtEnd: string[]
|
|
1078
|
+
if (hasCrossedBoundary) {
|
|
1079
|
+
const nextMarkTypes = new Set((nextNode?.marks || []).map((mark: any) => mark.type))
|
|
1080
|
+
|
|
1081
|
+
marksToOpen.forEach(({ type }) => {
|
|
1082
|
+
if (nextMarkTypes.has(type) && this.getHtmlReopenTags(type)) {
|
|
1083
|
+
reopenWithHtmlOnNextOpen.add(type)
|
|
1084
|
+
}
|
|
1085
|
+
})
|
|
1086
|
+
|
|
1087
|
+
marksToCloseAtEnd = [
|
|
1088
|
+
...marksToOpen.map(m => m.type), // inner (opened here) — close first
|
|
1089
|
+
...activeMarksClosingHere, // outer (were active before) — close last
|
|
1090
|
+
]
|
|
1091
|
+
} else {
|
|
1092
|
+
marksToCloseAtEnd = findMarksToCloseAtEnd(activeMarks, currentMarks, nextNode, this.markSetsEqual.bind(this))
|
|
1093
|
+
}
|
|
974
1094
|
|
|
975
1095
|
// Extract trailing whitespace before closing marks to prevent invalid markdown like "**text **"
|
|
976
1096
|
let trailingWhitespace = ''
|
|
@@ -983,12 +1103,13 @@ export class MarkdownManager {
|
|
|
983
1103
|
}
|
|
984
1104
|
|
|
985
1105
|
marksToCloseAtEnd.forEach(markType => {
|
|
986
|
-
const mark = activeMarks.get(markType)
|
|
987
|
-
const closeMarkdown = this.getMarkClosing(markType, mark)
|
|
1106
|
+
const mark = activeMarks.get(markType) ?? currentMarks.get(markType)
|
|
1107
|
+
const closeMarkdown = this.getMarkClosing(markType, mark, markOpeningModes.get(markType))
|
|
988
1108
|
if (closeMarkdown) {
|
|
989
1109
|
textContent += closeMarkdown
|
|
990
1110
|
}
|
|
991
1111
|
activeMarks.delete(markType)
|
|
1112
|
+
markOpeningModes.delete(markType)
|
|
992
1113
|
})
|
|
993
1114
|
|
|
994
1115
|
// Add trailing whitespace after the mark closing
|
|
@@ -999,9 +1120,13 @@ export class MarkdownManager {
|
|
|
999
1120
|
} else {
|
|
1000
1121
|
// For non-text nodes, close all active marks before rendering, then reopen after
|
|
1001
1122
|
const marksToReopen = new Map(activeMarks)
|
|
1123
|
+
const openingModesToReopen = new Map(markOpeningModes)
|
|
1002
1124
|
|
|
1003
1125
|
// Close all marks before the node
|
|
1004
|
-
const beforeMarkdown = closeMarksBeforeNode(activeMarks,
|
|
1126
|
+
const beforeMarkdown = closeMarksBeforeNode(activeMarks, (markType, mark) => {
|
|
1127
|
+
return this.getMarkClosing(markType, mark, markOpeningModes.get(markType))
|
|
1128
|
+
})
|
|
1129
|
+
markOpeningModes.clear()
|
|
1005
1130
|
|
|
1006
1131
|
// Render the node
|
|
1007
1132
|
const nodeContent = this.renderNodeToMarkdown(node, parentNode, i, level)
|
|
@@ -1011,7 +1136,11 @@ export class MarkdownManager {
|
|
|
1011
1136
|
const afterMarkdown =
|
|
1012
1137
|
node.type === 'hardBreak'
|
|
1013
1138
|
? ''
|
|
1014
|
-
: reopenMarksAfterNode(marksToReopen, activeMarks,
|
|
1139
|
+
: reopenMarksAfterNode(marksToReopen, activeMarks, (markType, mark) => {
|
|
1140
|
+
const openingMode = openingModesToReopen.get(markType) ?? 'markdown'
|
|
1141
|
+
markOpeningModes.set(markType, openingMode)
|
|
1142
|
+
return this.getMarkOpening(markType, mark, openingMode)
|
|
1143
|
+
})
|
|
1015
1144
|
|
|
1016
1145
|
result.push(beforeMarkdown + nodeContent + afterMarkdown)
|
|
1017
1146
|
}
|
|
@@ -1023,7 +1152,11 @@ export class MarkdownManager {
|
|
|
1023
1152
|
/**
|
|
1024
1153
|
* Get the opening markdown syntax for a mark type.
|
|
1025
1154
|
*/
|
|
1026
|
-
private getMarkOpening(markType: string, mark: any): string {
|
|
1155
|
+
private getMarkOpening(markType: string, mark: any, openingMode: 'markdown' | 'html' = 'markdown'): string {
|
|
1156
|
+
if (openingMode === 'html') {
|
|
1157
|
+
return this.getHtmlReopenTags(markType)?.open || ''
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1027
1160
|
const handlers = this.getHandlersForNodeType(markType)
|
|
1028
1161
|
const handler = handlers.length > 0 ? handlers[0] : undefined
|
|
1029
1162
|
if (!handler || !handler.renderMarkdown) {
|
|
@@ -1045,6 +1178,7 @@ export class MarkdownManager {
|
|
|
1045
1178
|
syntheticNode,
|
|
1046
1179
|
{
|
|
1047
1180
|
renderChildren: () => placeholder,
|
|
1181
|
+
renderChild: () => placeholder,
|
|
1048
1182
|
indent: (content: string) => content,
|
|
1049
1183
|
wrapInBlock: (prefix: string, content: string) => prefix + content,
|
|
1050
1184
|
},
|
|
@@ -1062,7 +1196,11 @@ export class MarkdownManager {
|
|
|
1062
1196
|
/**
|
|
1063
1197
|
* Get the closing markdown syntax for a mark type.
|
|
1064
1198
|
*/
|
|
1065
|
-
private getMarkClosing(markType: string, mark: any): string {
|
|
1199
|
+
private getMarkClosing(markType: string, mark: any, openingMode: 'markdown' | 'html' = 'markdown'): string {
|
|
1200
|
+
if (openingMode === 'html') {
|
|
1201
|
+
return this.getHtmlReopenTags(markType)?.close || ''
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1066
1204
|
const handlers = this.getHandlersForNodeType(markType)
|
|
1067
1205
|
const handler = handlers.length > 0 ? handlers[0] : undefined
|
|
1068
1206
|
if (!handler || !handler.renderMarkdown) {
|
|
@@ -1083,6 +1221,7 @@ export class MarkdownManager {
|
|
|
1083
1221
|
syntheticNode,
|
|
1084
1222
|
{
|
|
1085
1223
|
renderChildren: () => placeholder,
|
|
1224
|
+
renderChild: () => placeholder,
|
|
1086
1225
|
indent: (content: string) => content,
|
|
1087
1226
|
wrapInBlock: (prefix: string, content: string) => prefix + content,
|
|
1088
1227
|
},
|
|
@@ -1098,6 +1237,17 @@ export class MarkdownManager {
|
|
|
1098
1237
|
}
|
|
1099
1238
|
}
|
|
1100
1239
|
|
|
1240
|
+
/**
|
|
1241
|
+
* Returns the inline HTML tags an extension exposes for overlap-boundary
|
|
1242
|
+
* reopen handling, if that mark explicitly opted into HTML reopen mode.
|
|
1243
|
+
*/
|
|
1244
|
+
private getHtmlReopenTags(markType: string): { open: string; close: string } | undefined {
|
|
1245
|
+
const handlers = this.getHandlersForNodeType(markType)
|
|
1246
|
+
const handler = handlers.length > 0 ? handlers[0] : undefined
|
|
1247
|
+
|
|
1248
|
+
return handler?.htmlReopen
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1101
1251
|
/**
|
|
1102
1252
|
* Check if two mark sets are equal.
|
|
1103
1253
|
*/
|
package/src/utils.ts
CHANGED
|
@@ -77,14 +77,16 @@ export function findMarksToCloseAtEnd(
|
|
|
77
77
|
if (isLastNode || nextNodeHasNoMarks || nextNodeHasDifferentMarks) {
|
|
78
78
|
if (nextNode && nextNode.type === 'text' && nextNode.marks) {
|
|
79
79
|
const nextMarks = new Map(nextNode.marks.map((mark: any) => [mark.type, mark]))
|
|
80
|
-
Array.from(activeMarks.keys())
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
Array.from(activeMarks.keys())
|
|
81
|
+
.reverse()
|
|
82
|
+
.forEach(markType => {
|
|
83
|
+
if (!nextMarks.has(markType)) {
|
|
84
|
+
marksToCloseAtEnd.push(markType)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
85
87
|
} else if (isLastNode || nextNodeHasNoMarks) {
|
|
86
88
|
// Close all active marks
|
|
87
|
-
marksToCloseAtEnd.push(...Array.from(activeMarks.keys()))
|
|
89
|
+
marksToCloseAtEnd.push(...Array.from(activeMarks.keys()).reverse())
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
|