@tiptap/extension-list 3.26.1 → 3.27.1
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/dist/index.cjs +377 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -2
- package/dist/index.d.ts +67 -2
- package/dist/index.js +366 -17
- package/dist/index.js.map +1 -1
- package/dist/item/index.cjs +67 -2
- package/dist/item/index.cjs.map +1 -1
- package/dist/item/index.js +67 -2
- package/dist/item/index.js.map +1 -1
- package/dist/kit/index.cjs +355 -16
- package/dist/kit/index.cjs.map +1 -1
- package/dist/kit/index.js +355 -16
- package/dist/kit/index.js.map +1 -1
- package/dist/ordered-list/index.cjs +373 -16
- package/dist/ordered-list/index.cjs.map +1 -1
- package/dist/ordered-list/index.d.cts +67 -2
- package/dist/ordered-list/index.d.ts +67 -2
- package/dist/ordered-list/index.js +362 -15
- package/dist/ordered-list/index.js.map +1 -1
- package/package.json +8 -5
- package/src/item/list-item.ts +5 -1
- package/src/ordered-list/index.ts +12 -0
- package/src/ordered-list/ordered-list.ts +145 -10
- package/src/ordered-list/roman.ts +295 -0
- package/src/ordered-list/utils.ts +97 -9
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
const ROMAN_NUMERALS: [number, string][] = [
|
|
2
|
+
[1000, 'm'],
|
|
3
|
+
[900, 'cm'],
|
|
4
|
+
[500, 'd'],
|
|
5
|
+
[400, 'cd'],
|
|
6
|
+
[100, 'c'],
|
|
7
|
+
[90, 'xc'],
|
|
8
|
+
[50, 'l'],
|
|
9
|
+
[40, 'xl'],
|
|
10
|
+
[10, 'x'],
|
|
11
|
+
[9, 'ix'],
|
|
12
|
+
[5, 'v'],
|
|
13
|
+
[4, 'iv'],
|
|
14
|
+
[1, 'i'],
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const ALPHA_NUMERALS = 'abcdefghijklmnopqrstuvwxyz'
|
|
18
|
+
|
|
19
|
+
/** Alpha list markers support at most 2 letters (a–z, aa–zz), matching {@link fromAlpha}. */
|
|
20
|
+
export const ORDERED_LIST_ALPHA_MARKER_PATTERN = '[a-zA-Z]{1,2}'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha.
|
|
24
|
+
* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha.
|
|
25
|
+
*/
|
|
26
|
+
export const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${ORDERED_LIST_ALPHA_MARKER_PATTERN}`
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert a number to lowercase roman numerals.
|
|
30
|
+
* @example toRoman(1) // 'i'
|
|
31
|
+
* @example toRoman(4) // 'iv'
|
|
32
|
+
*/
|
|
33
|
+
export function toRoman(num: number): string {
|
|
34
|
+
let remaining = num
|
|
35
|
+
let result = ''
|
|
36
|
+
|
|
37
|
+
for (const [value, numeral] of ROMAN_NUMERALS) {
|
|
38
|
+
while (remaining >= value) {
|
|
39
|
+
result += numeral
|
|
40
|
+
remaining -= value
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Convert a number to uppercase roman numerals.
|
|
49
|
+
* @example toRomanUpper(1) // 'I'
|
|
50
|
+
* @example toRomanUpper(4) // 'IV'
|
|
51
|
+
*/
|
|
52
|
+
export function toRomanUpper(num: number): string {
|
|
53
|
+
return toRoman(num).toUpperCase()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fromRoman(roman: string): number {
|
|
57
|
+
const lower = roman.toLowerCase()
|
|
58
|
+
let index = 0
|
|
59
|
+
let result = 0
|
|
60
|
+
|
|
61
|
+
while (index < lower.length) {
|
|
62
|
+
let matched = false
|
|
63
|
+
|
|
64
|
+
for (const [value, numeral] of ROMAN_NUMERALS) {
|
|
65
|
+
if (lower.startsWith(numeral, index)) {
|
|
66
|
+
result += value
|
|
67
|
+
index += numeral.length
|
|
68
|
+
matched = true
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!matched) {
|
|
74
|
+
return 0
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isValidRoman(marker: string): boolean {
|
|
82
|
+
if (!/^[ivxlcdmIVXLCDM]+$/.test(marker)) {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const value = fromRoman(marker)
|
|
87
|
+
|
|
88
|
+
if (value <= 0) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const expected = marker === marker.toLowerCase() ? toRoman(value) : toRomanUpper(value)
|
|
93
|
+
|
|
94
|
+
return expected === marker
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function fromAlpha(marker: string): number {
|
|
98
|
+
const lower = marker.toLowerCase()
|
|
99
|
+
|
|
100
|
+
if (lower.length === 1) {
|
|
101
|
+
return lower.charCodeAt(0) - 'a'.charCodeAt(0) + 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (lower.length === 2) {
|
|
105
|
+
const first = lower.charCodeAt(0) - 'a'.charCodeAt(0)
|
|
106
|
+
const second = lower.charCodeAt(1) - 'a'.charCodeAt(0)
|
|
107
|
+
|
|
108
|
+
return (first + 1) * 26 + second + 1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toRomanAlpha(num: number): string {
|
|
115
|
+
if (num <= 26) {
|
|
116
|
+
return ALPHA_NUMERALS[num - 1]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const first = Math.floor((num - 1) / 26) - 1
|
|
120
|
+
const second = (num - 1) % 26
|
|
121
|
+
|
|
122
|
+
if (first < 0) {
|
|
123
|
+
return ALPHA_NUMERALS[second]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return ALPHA_NUMERALS[first] + ALPHA_NUMERALS[second]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract the list marker type from a marker string.
|
|
131
|
+
* Supports "1", "a", "A", "i", "I" marker styles.
|
|
132
|
+
*
|
|
133
|
+
* @param marker The text content of the list marker (e.g. "a", "1", "iii", "b")
|
|
134
|
+
* @returns The normalized type string, or undefined for default numeric type
|
|
135
|
+
*/
|
|
136
|
+
export function detectMarkerType(marker: string): string | undefined {
|
|
137
|
+
if (!marker || /^\d+$/.test(marker)) {
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (isValidRoman(marker)) {
|
|
142
|
+
return marker === marker.toLowerCase() ? 'i' : 'I'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (/^[a-z]{1,2}$/.test(marker)) {
|
|
146
|
+
return 'a'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (/^[A-Z]{1,2}$/.test(marker)) {
|
|
150
|
+
return 'A'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Convert a list marker string to its numeric start position.
|
|
158
|
+
*
|
|
159
|
+
* @param marker The text content of the list marker (e.g. "3", "b", "II")
|
|
160
|
+
* @returns The 1-based start value for the ordered list
|
|
161
|
+
*/
|
|
162
|
+
export function markerToStart(marker: string): number {
|
|
163
|
+
if (/^\d+$/.test(marker)) {
|
|
164
|
+
return parseInt(marker, 10)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const type = detectMarkerType(marker)
|
|
168
|
+
|
|
169
|
+
if (type === 'i' || type === 'I') {
|
|
170
|
+
return fromRoman(marker)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (type === 'a' || type === 'A') {
|
|
174
|
+
const start = fromAlpha(marker)
|
|
175
|
+
|
|
176
|
+
return start > 0 ? start : 1
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const parsed = parseInt(marker, 10)
|
|
180
|
+
|
|
181
|
+
return Number.isNaN(parsed) ? 1 : parsed
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function startToMarker(type: string, start: number): string {
|
|
185
|
+
if (type === 'numeric') {
|
|
186
|
+
return String(start)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
switch (type) {
|
|
190
|
+
case 'a':
|
|
191
|
+
return toRomanAlpha(start)
|
|
192
|
+
case 'A':
|
|
193
|
+
return toRomanAlpha(start).toUpperCase()
|
|
194
|
+
case 'i':
|
|
195
|
+
return toRoman(start)
|
|
196
|
+
case 'I':
|
|
197
|
+
return toRomanUpper(start)
|
|
198
|
+
default:
|
|
199
|
+
return String(start)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Returns true when all markers share the same style and increment by 1.
|
|
205
|
+
* Style is inferred from the first marker so ambiguous letters (e.g. "c", "i")
|
|
206
|
+
* are not re-classified differently on later lines.
|
|
207
|
+
*/
|
|
208
|
+
export function areOrderedListMarkersSequential(markers: string[]): boolean {
|
|
209
|
+
if (markers.length === 0) {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const firstType = detectMarkerType(markers[0]) ?? 'numeric'
|
|
214
|
+
const firstStart = markerToStart(markers[0])
|
|
215
|
+
|
|
216
|
+
if (firstStart < 1) {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < markers.length; i++) {
|
|
221
|
+
const expected = startToMarker(firstType, firstStart + i)
|
|
222
|
+
|
|
223
|
+
if (markers[i] !== expected) {
|
|
224
|
+
return false
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface ParsedListMarker {
|
|
232
|
+
type?: string
|
|
233
|
+
start: number
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Parse a list marker into HTML ordered-list attrs (type + start).
|
|
238
|
+
*/
|
|
239
|
+
export function parseListMarker(marker: string): ParsedListMarker {
|
|
240
|
+
return {
|
|
241
|
+
type: detectMarkerType(marker),
|
|
242
|
+
start: markerToStart(marker),
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Build orderedList node attrs from the first list item marker.
|
|
248
|
+
*/
|
|
249
|
+
export function buildOrderedListAttrsFromMarker(marker: string): Record<string, string | number> {
|
|
250
|
+
const { type, start } = parseListMarker(marker)
|
|
251
|
+
const attrs: Record<string, string | number> = {}
|
|
252
|
+
|
|
253
|
+
if (type) {
|
|
254
|
+
attrs.type = type
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (start !== 1) {
|
|
258
|
+
attrs.start = start
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return attrs
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Returns the list marker prefix for a given item at a given index.
|
|
266
|
+
*
|
|
267
|
+
* @param type The list type attribute (e.g. "a", "A", "i", "I", null/undefined for default)
|
|
268
|
+
* @param index The zero-based index of the list item
|
|
269
|
+
* @param separator The separator to use (default: ". ")
|
|
270
|
+
* @returns The marker string (e.g. "a. ", "I. ", "1. ")
|
|
271
|
+
*/
|
|
272
|
+
export function getListMarker(
|
|
273
|
+
type: string | null | undefined,
|
|
274
|
+
index: number,
|
|
275
|
+
separator = '. ',
|
|
276
|
+
): string {
|
|
277
|
+
const position = index + 1
|
|
278
|
+
|
|
279
|
+
if (!type || type === '1') {
|
|
280
|
+
return `${position}${separator}`
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
switch (type) {
|
|
284
|
+
case 'a':
|
|
285
|
+
return `${toRomanAlpha(position)}${separator}`
|
|
286
|
+
case 'A':
|
|
287
|
+
return `${toRomanAlpha(position).toUpperCase()}${separator}`
|
|
288
|
+
case 'i':
|
|
289
|
+
return `${toRoman(position)}${separator}`
|
|
290
|
+
case 'I':
|
|
291
|
+
return `${toRomanUpper(position)}${separator}`
|
|
292
|
+
default:
|
|
293
|
+
return `${position}${separator}`
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -5,12 +5,33 @@ import type {
|
|
|
5
5
|
MarkdownToken,
|
|
6
6
|
} from '@tiptap/core'
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
areOrderedListMarkersSequential,
|
|
10
|
+
buildOrderedListAttrsFromMarker,
|
|
11
|
+
detectMarkerType,
|
|
12
|
+
markerToStart,
|
|
13
|
+
ORDERED_LIST_MARKER_PATTERN,
|
|
14
|
+
} from './roman.js'
|
|
15
|
+
|
|
16
|
+
export { ORDERED_LIST_MARKER_PATTERN }
|
|
17
|
+
|
|
8
18
|
/**
|
|
9
19
|
* Matches an ordered list item line with optional leading whitespace.
|
|
10
|
-
* Captures: (1) indentation spaces, (2) item number,
|
|
11
|
-
*
|
|
20
|
+
* Captures: (1) indentation spaces, (2) item marker (number, letter, or roman numeral),
|
|
21
|
+
* (3) separator (. or )), (4) content after marker
|
|
22
|
+
*
|
|
23
|
+
* Examples: "1. Item", " a) Nested item", " I. Roman item", "iii. Another", "aa. Item 27"
|
|
12
24
|
*/
|
|
13
|
-
const ORDERED_LIST_ITEM_REGEX =
|
|
25
|
+
export const ORDERED_LIST_ITEM_REGEX = new RegExp(
|
|
26
|
+
`^(\\s*)(${ORDERED_LIST_MARKER_PATTERN})([.)])\\s+(.*)$`,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Matches the start of an ordered list line (used by markdown tokenizer).
|
|
31
|
+
*/
|
|
32
|
+
export const ORDERED_LIST_LINE_START_REGEX = new RegExp(
|
|
33
|
+
`^(\\s*)(${ORDERED_LIST_MARKER_PATTERN})([.)])\\s+`,
|
|
34
|
+
)
|
|
14
35
|
|
|
15
36
|
/**
|
|
16
37
|
* Matches any line that starts with whitespace (indented content).
|
|
@@ -24,19 +45,23 @@ const INDENTED_LINE_REGEX = /^\s/
|
|
|
24
45
|
export interface OrderedListItem {
|
|
25
46
|
indent: number
|
|
26
47
|
number: number
|
|
48
|
+
type?: string
|
|
27
49
|
content: string
|
|
28
50
|
contentLines: string[]
|
|
29
51
|
raw: string
|
|
30
52
|
}
|
|
31
53
|
|
|
54
|
+
function isOrderedListMarkerLine(line: string): boolean {
|
|
55
|
+
return ORDERED_LIST_ITEM_REGEX.test(line.trimStart())
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
function isBlockContentLine(line: string): boolean {
|
|
33
59
|
const trimmedLine = line.trimStart()
|
|
34
60
|
|
|
35
61
|
return (
|
|
36
62
|
// oxlint-disable-next-line prefer-string-starts-ends-with
|
|
37
63
|
/^[-+*]\s+/.test(trimmedLine) ||
|
|
38
|
-
|
|
39
|
-
/^\d+\.\s+/.test(trimmedLine) ||
|
|
64
|
+
isOrderedListMarkerLine(trimmedLine) ||
|
|
40
65
|
// oxlint-disable-next-line prefer-string-starts-ends-with
|
|
41
66
|
/^>\s?/.test(trimmedLine) ||
|
|
42
67
|
// oxlint-disable-next-line prefer-string-starts-ends-with
|
|
@@ -102,8 +127,13 @@ export function collectOrderedListItems(lines: string[]): [OrderedListItem[], nu
|
|
|
102
127
|
break
|
|
103
128
|
}
|
|
104
129
|
|
|
105
|
-
const [, indent,
|
|
130
|
+
const [, indent, marker, _separator, content] = match
|
|
106
131
|
const indentLevel = indent.length
|
|
132
|
+
const number = parseInt(marker, 10)
|
|
133
|
+
|
|
134
|
+
const markerType = isNaN(number) ? detectMarkerType(marker) : undefined
|
|
135
|
+
const itemNumber = isNaN(number) ? markerToStart(marker) : number
|
|
136
|
+
|
|
107
137
|
const itemContentLines = [content]
|
|
108
138
|
let nextLineIndex = currentLineIndex + 1
|
|
109
139
|
const itemLines = [line]
|
|
@@ -127,9 +157,13 @@ export function collectOrderedListItems(lines: string[]): [OrderedListItem[], nu
|
|
|
127
157
|
sawBlankLine = true
|
|
128
158
|
nextLineIndex += 1
|
|
129
159
|
} else if (nextLine.match(INDENTED_LINE_REGEX)) {
|
|
130
|
-
// Indented content - part of this item (but not a list item)
|
|
160
|
+
// Indented content - part of this item (but not a list item).
|
|
161
|
+
// Strip the indentation only up to the whitespace that is actually present,
|
|
162
|
+
// so an under-indented line (e.g. a single leading space) keeps its first character.
|
|
163
|
+
const leadingWhitespace = nextLine.length - nextLine.trimStart().length
|
|
164
|
+
const contentIndent = indentLevel + marker.length + 1
|
|
131
165
|
itemLines.push(nextLine)
|
|
132
|
-
itemContentLines.push(nextLine.slice(
|
|
166
|
+
itemContentLines.push(nextLine.slice(Math.min(leadingWhitespace, contentIndent)))
|
|
133
167
|
nextLineIndex += 1
|
|
134
168
|
} else {
|
|
135
169
|
if (sawBlankLine) {
|
|
@@ -144,7 +178,8 @@ export function collectOrderedListItems(lines: string[]): [OrderedListItem[], nu
|
|
|
144
178
|
|
|
145
179
|
listItems.push({
|
|
146
180
|
indent: indentLevel,
|
|
147
|
-
number:
|
|
181
|
+
number: itemNumber,
|
|
182
|
+
type: markerType,
|
|
148
183
|
content: itemContentLines.join('\n').trim(),
|
|
149
184
|
contentLines: itemContentLines,
|
|
150
185
|
raw: itemLines.join('\n'),
|
|
@@ -157,6 +192,58 @@ export function collectOrderedListItems(lines: string[]): [OrderedListItem[], nu
|
|
|
157
192
|
return [listItems, consumed]
|
|
158
193
|
}
|
|
159
194
|
|
|
195
|
+
const PLAIN_TEXT_ORDERED_LIST_LINE_REGEX = new RegExp(
|
|
196
|
+
`^(${ORDERED_LIST_MARKER_PATTERN})([.)])\\s+(.+)$`,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Parse plain-text pasted ordered list lines into JSONContent, or null if not a typed list.
|
|
201
|
+
*/
|
|
202
|
+
export function parsePlainTextOrderedListPaste(text: string): JSONContent | null {
|
|
203
|
+
const lines = text.split('\n').filter(l => l.trim().length > 0)
|
|
204
|
+
|
|
205
|
+
if (lines.length === 0) {
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const parsedItems: Array<{ marker: string; content: string }> = []
|
|
210
|
+
|
|
211
|
+
for (const line of lines) {
|
|
212
|
+
const match = line.trim().match(PLAIN_TEXT_ORDERED_LIST_LINE_REGEX)
|
|
213
|
+
|
|
214
|
+
if (!match) {
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
parsedItems.push({
|
|
219
|
+
marker: match[1],
|
|
220
|
+
content: match[3],
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const markers = parsedItems.map(item => item.marker)
|
|
225
|
+
|
|
226
|
+
if (!areOrderedListMarkersSequential(markers)) {
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const attrs = buildOrderedListAttrsFromMarker(parsedItems[0].marker)
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
type: 'orderedList',
|
|
234
|
+
attrs,
|
|
235
|
+
content: parsedItems.map(item => ({
|
|
236
|
+
type: 'listItem',
|
|
237
|
+
content: [
|
|
238
|
+
{
|
|
239
|
+
type: 'paragraph',
|
|
240
|
+
content: [{ type: 'text', text: item.content }],
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
})),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
160
247
|
/**
|
|
161
248
|
* Recursively builds a nested structure from a flat array of list items
|
|
162
249
|
* based on their indentation levels. Creates proper markdown tokens with
|
|
@@ -223,6 +310,7 @@ export function buildNestedStructure(
|
|
|
223
310
|
type: 'list',
|
|
224
311
|
ordered: true,
|
|
225
312
|
start: nestedItems[0].number,
|
|
313
|
+
typeMarker: nestedItems[0].type,
|
|
226
314
|
items: nestedListItems,
|
|
227
315
|
raw: nestedItems.map(nestedItem => nestedItem.raw).join('\n'),
|
|
228
316
|
})
|