@portabletext/block-tools 3.4.1 → 3.5.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.
@@ -0,0 +1,225 @@
1
+ import {
2
+ isTextBlock,
3
+ type PortableTextObject,
4
+ type PortableTextSpan,
5
+ type Schema,
6
+ } from '@portabletext/schema'
7
+ import {flattenNestedBlocks} from '../HtmlDeserializer/flatten-nested-blocks'
8
+ import {isElement, tagName} from '../HtmlDeserializer/helpers'
9
+ import type {
10
+ ArbitraryTypedObject,
11
+ DeserializerRule,
12
+ TypedObject,
13
+ } from '../types'
14
+
15
+ /**
16
+ * An opinionated `DeserializerRule` that flattens tables in a way that repeats
17
+ * the header row for each cell in the row.
18
+ *
19
+ * @example
20
+ * ```html
21
+ * <table>
22
+ * <thead>
23
+ * <tr>
24
+ * <th>Header 1</th>
25
+ * <th>Header 2</th>
26
+ * </tr>
27
+ * </thead>
28
+ * <tbody>
29
+ * <tr>
30
+ * <td>Cell 1</td>
31
+ * <td>Cell 2</td>
32
+ * </tr>
33
+ * </tbody>
34
+ * </table>
35
+ * ```
36
+ * Turns into
37
+ * ```json
38
+ * [
39
+ * {
40
+ * _type: 'block',
41
+ * children: [
42
+ * {
43
+ * _type: 'text',
44
+ * text: 'Header 1'
45
+ * },
46
+ * {
47
+ * _type: 'text',
48
+ * text: 'Cell 1'
49
+ * }
50
+ * ]
51
+ * },
52
+ * {
53
+ * _type: 'block',
54
+ * children: [
55
+ * {
56
+ * _type: 'text',
57
+ * text: 'Header 2'
58
+ * },
59
+ * {
60
+ * _type: 'text',
61
+ * text: 'Cell 2'
62
+ * }
63
+ * ]
64
+ * }
65
+ * ]
66
+ * ```
67
+ *
68
+ * Use the `separator` option to control if a child element should separate
69
+ * headers and cells.
70
+ *
71
+ * @beta
72
+ */
73
+ export function createFlattenTableRule({
74
+ schema,
75
+ separator,
76
+ }: {
77
+ schema: Schema
78
+ separator?: () =>
79
+ | (Omit<PortableTextSpan, '_key'> & {_key?: string})
80
+ | (Omit<PortableTextObject, '_key'> & {_key?: string})
81
+ | undefined
82
+ }): DeserializerRule {
83
+ return {
84
+ deserialize: (node, next) => {
85
+ if (!isElement(node) || tagName(node) !== 'table') {
86
+ return undefined
87
+ }
88
+
89
+ const thead = node.querySelector('thead')
90
+ let headerRow = thead?.querySelector('tr')
91
+ const tbody = node.querySelector('tbody')
92
+ let bodyRows = tbody ? [...tbody.querySelectorAll('tr')] : []
93
+
94
+ if (!headerRow || !bodyRows) {
95
+ // If there is not thead or tbody, we look at the column count. If the
96
+ // column count is greater than 2 then we infer that the first row is
97
+ // the header row and the rest are the body rows.
98
+
99
+ const columnCounts = [...node.querySelectorAll('tr')].map((row) => {
100
+ const cells = row.querySelectorAll('td')
101
+ return cells.length
102
+ })
103
+
104
+ const firstColumnCount = columnCounts[0]
105
+
106
+ if (
107
+ !firstColumnCount ||
108
+ !columnCounts.every((count) => count === firstColumnCount)
109
+ ) {
110
+ return undefined
111
+ }
112
+
113
+ if (firstColumnCount < 3) {
114
+ return undefined
115
+ }
116
+
117
+ // Now we know that all rows have the same column count and that
118
+ // count is >2
119
+ const rows = [...node.querySelectorAll('tr')]
120
+ headerRow = rows.slice(0, 1)[0]
121
+ bodyRows = rows.slice(1)
122
+ }
123
+
124
+ if (!headerRow) {
125
+ return undefined
126
+ }
127
+
128
+ const headerCells = headerRow.querySelectorAll('th, td')
129
+ const headerResults = [...headerCells].map((headerCell) =>
130
+ next(headerCell),
131
+ )
132
+
133
+ // Process tbody rows and combine with headers
134
+ const rows: TypedObject[] = []
135
+
136
+ for (const row of bodyRows) {
137
+ const cells = row.querySelectorAll('td')
138
+
139
+ let cellIndex = 0
140
+ for (const cell of cells) {
141
+ const result = next(cell)
142
+
143
+ if (!result) {
144
+ cellIndex++
145
+ continue
146
+ }
147
+
148
+ const headerResult = headerResults[cellIndex]
149
+
150
+ if (!headerResult) {
151
+ // If we can't find a corresponding header, then we just push
152
+ // the deserialized cell as is.
153
+ if (Array.isArray(result)) {
154
+ rows.push(...result)
155
+ } else {
156
+ rows.push(result)
157
+ }
158
+ cellIndex++
159
+ continue
160
+ }
161
+
162
+ const flattenedHeaderResult = flattenNestedBlocks(
163
+ {schema},
164
+ (Array.isArray(headerResult)
165
+ ? headerResult
166
+ : [headerResult]) as Array<ArbitraryTypedObject>,
167
+ )
168
+ const firstFlattenedHeaderResult = flattenedHeaderResult[0]
169
+ const flattenedResult = flattenNestedBlocks(
170
+ {schema},
171
+ (Array.isArray(result)
172
+ ? result
173
+ : [result]) as Array<ArbitraryTypedObject>,
174
+ )
175
+ const firstFlattenedResult = flattenedResult[0]
176
+
177
+ if (
178
+ flattenedHeaderResult.length === 1 &&
179
+ isTextBlock({schema}, firstFlattenedHeaderResult) &&
180
+ flattenedResult.length === 1 &&
181
+ isTextBlock({schema}, firstFlattenedResult)
182
+ ) {
183
+ const separatorChild = separator?.()
184
+ // If the header result and the cell result are text blocks then
185
+ // we merge them together.
186
+ const mergedTextBlock = {
187
+ ...firstFlattenedHeaderResult,
188
+ children: [
189
+ ...firstFlattenedHeaderResult.children,
190
+ ...(separatorChild ? [separatorChild] : []),
191
+ ...firstFlattenedResult.children,
192
+ ],
193
+ markDefs: [
194
+ ...(firstFlattenedHeaderResult.markDefs ?? []),
195
+ ...(firstFlattenedResult.markDefs ?? []),
196
+ ],
197
+ }
198
+
199
+ rows.push(mergedTextBlock)
200
+ cellIndex++
201
+ continue
202
+ }
203
+
204
+ // Otherwise, we push the header result and the cell result as is.
205
+ if (Array.isArray(headerResult)) {
206
+ rows.push(...headerResult)
207
+ } else {
208
+ rows.push(headerResult)
209
+ }
210
+
211
+ if (Array.isArray(result)) {
212
+ rows.push(...result)
213
+ } else {
214
+ rows.push(result)
215
+ }
216
+
217
+ cellIndex++
218
+ }
219
+ }
220
+
221
+ // Return the processed rows as individual text blocks
222
+ return rows
223
+ },
224
+ }
225
+ }
@@ -0,0 +1 @@
1
+ export * from './flatten-tables'