@portabletext/block-tools 3.2.1 → 3.3.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/LICENSE +1 -1
- package/lib/index.cjs +140 -22
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +48 -2
- package/lib/index.d.ts +48 -2
- package/lib/index.js +139 -21
- package/lib/index.js.map +1 -1
- package/package.json +6 -6
- package/src/HtmlDeserializer/flatten-nested-blocks.test.ts +140 -0
- package/src/HtmlDeserializer/helpers.ts +113 -7
- package/src/HtmlDeserializer/index.ts +9 -2
- package/src/HtmlDeserializer/rules/gdocs.ts +9 -1
- package/src/HtmlDeserializer/rules/html.ts +76 -1
- package/src/HtmlDeserializer/rules/index.ts +2 -1
- package/src/index.ts +2 -2
- package/src/schema-matchers.ts +41 -0
- package/src/types.ts +6 -0
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { sanitySchemaToPortableTextSchema } from "@portabletext/sanity-bridge";
|
|
2
2
|
import flatten from "lodash/flatten.js";
|
|
3
|
+
import getRandomValues from "get-random-values-esm";
|
|
3
4
|
import isEqual from "lodash/isEqual.js";
|
|
4
5
|
import uniq from "lodash/uniq.js";
|
|
5
|
-
import getRandomValues from "get-random-values-esm";
|
|
6
6
|
function isArbitraryTypedObject(object) {
|
|
7
7
|
return isRecord(object) && typeof object._type == "string";
|
|
8
8
|
}
|
|
@@ -15,6 +15,19 @@ function isTextBlock(schema, block) {
|
|
|
15
15
|
function isSpan(schema, child) {
|
|
16
16
|
return !(!isArbitraryTypedObject(child) || child._type !== schema.span.name || typeof child.text != "string");
|
|
17
17
|
}
|
|
18
|
+
function keyGenerator() {
|
|
19
|
+
return randomKey(12);
|
|
20
|
+
}
|
|
21
|
+
function whatwgRNG(length = 16) {
|
|
22
|
+
const rnds8 = new Uint8Array(length);
|
|
23
|
+
return getRandomValues(rnds8), rnds8;
|
|
24
|
+
}
|
|
25
|
+
const byteToHex = [];
|
|
26
|
+
for (let i = 0; i < 256; ++i)
|
|
27
|
+
byteToHex[i] = (i + 256).toString(16).slice(1);
|
|
28
|
+
function randomKey(length) {
|
|
29
|
+
return whatwgRNG(length).reduce((str, n) => str + byteToHex[n], "").slice(0, length);
|
|
30
|
+
}
|
|
18
31
|
const objectToString = Object.prototype.toString;
|
|
19
32
|
function resolveJsType(val) {
|
|
20
33
|
switch (objectToString.call(val)) {
|
|
@@ -296,12 +309,67 @@ function defaultParseHtml() {
|
|
|
296
309
|
);
|
|
297
310
|
return (html) => new DOMParser().parseFromString(html, "text/html");
|
|
298
311
|
}
|
|
299
|
-
function flattenNestedBlocks(
|
|
312
|
+
function flattenNestedBlocks(context, blocks2) {
|
|
300
313
|
let depth = 0;
|
|
301
314
|
const flattened = [], traverse = (nodes) => {
|
|
302
315
|
const toRemove = [];
|
|
303
316
|
nodes.forEach((node) => {
|
|
304
|
-
|
|
317
|
+
if (depth === 0)
|
|
318
|
+
if (context.schema.blockObjects.length > 0 && isTextBlock(context.schema, node)) {
|
|
319
|
+
const hasBlockObjects = node.children.some((child) => context.schema.blockObjects.some(
|
|
320
|
+
(blockObject) => blockObject.name === child._type
|
|
321
|
+
)), hasBlocks = node.children.some(
|
|
322
|
+
(child) => child._type === "__block"
|
|
323
|
+
);
|
|
324
|
+
if (hasBlockObjects || hasBlocks) {
|
|
325
|
+
node.children.reduce(
|
|
326
|
+
(slices, child) => {
|
|
327
|
+
const knownInlineObject = context.schema.inlineObjects.some(
|
|
328
|
+
(inlineObject) => inlineObject.name === child._type
|
|
329
|
+
), knownBlockObject = context.schema.blockObjects.some(
|
|
330
|
+
(blockObject) => blockObject.name === child._type
|
|
331
|
+
), lastSlice = slices.pop();
|
|
332
|
+
return !isSpan(context.schema, child) && !knownInlineObject && knownBlockObject ? [
|
|
333
|
+
...slices,
|
|
334
|
+
...lastSlice ? [lastSlice] : [],
|
|
335
|
+
{ type: "block object", block: child }
|
|
336
|
+
] : child._type === "__block" ? [
|
|
337
|
+
...slices,
|
|
338
|
+
...lastSlice ? [lastSlice] : [],
|
|
339
|
+
{
|
|
340
|
+
type: "block object",
|
|
341
|
+
block: child.block
|
|
342
|
+
}
|
|
343
|
+
] : lastSlice && lastSlice.type === "children" ? [
|
|
344
|
+
...slices,
|
|
345
|
+
{
|
|
346
|
+
type: "children",
|
|
347
|
+
children: [...lastSlice.children, child]
|
|
348
|
+
}
|
|
349
|
+
] : [
|
|
350
|
+
...slices,
|
|
351
|
+
...lastSlice ? [lastSlice] : [],
|
|
352
|
+
{ type: "children", children: [child] }
|
|
353
|
+
];
|
|
354
|
+
},
|
|
355
|
+
[]
|
|
356
|
+
).forEach((slice) => {
|
|
357
|
+
if (slice.type === "block object")
|
|
358
|
+
flattened.push(slice.block);
|
|
359
|
+
else if (slice.children.length > 0) {
|
|
360
|
+
const newBlock = {
|
|
361
|
+
...node,
|
|
362
|
+
children: slice.children
|
|
363
|
+
};
|
|
364
|
+
flattened.push(newBlock);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
} else
|
|
369
|
+
flattened.push(node);
|
|
370
|
+
} else
|
|
371
|
+
flattened.push(node);
|
|
372
|
+
isTextBlock(context.schema, node) && (depth > 0 && (toRemove.push(node), flattened.push(node)), depth++, traverse(node.children)), node._type === "__block" && (toRemove.push(node), flattened.push(node.block));
|
|
305
373
|
}), toRemove.forEach((node) => {
|
|
306
374
|
nodes.splice(nodes.indexOf(node), 1);
|
|
307
375
|
}), depth--;
|
|
@@ -451,8 +519,10 @@ function getBlockStyle(schema, el) {
|
|
|
451
519
|
function createGDocsRules(schema) {
|
|
452
520
|
return [
|
|
453
521
|
{
|
|
454
|
-
deserialize(el) {
|
|
522
|
+
deserialize(el, next) {
|
|
455
523
|
if (isElement(el) && tagName(el) === "span" && isGoogleDocs(el)) {
|
|
524
|
+
if (!el.textContent)
|
|
525
|
+
return !el.previousSibling && !el.nextSibling && el.setAttribute("data-lonely-child", "true"), next(el.childNodes);
|
|
456
526
|
const span = {
|
|
457
527
|
...DEFAULT_SPAN,
|
|
458
528
|
marks: [],
|
|
@@ -495,19 +565,6 @@ function createGDocsRules(schema) {
|
|
|
495
565
|
}
|
|
496
566
|
];
|
|
497
567
|
}
|
|
498
|
-
function keyGenerator() {
|
|
499
|
-
return randomKey(12);
|
|
500
|
-
}
|
|
501
|
-
function whatwgRNG(length = 16) {
|
|
502
|
-
const rnds8 = new Uint8Array(length);
|
|
503
|
-
return getRandomValues(rnds8), rnds8;
|
|
504
|
-
}
|
|
505
|
-
const byteToHex = [];
|
|
506
|
-
for (let i = 0; i < 256; ++i)
|
|
507
|
-
byteToHex[i] = (i + 256).toString(16).slice(1);
|
|
508
|
-
function randomKey(length) {
|
|
509
|
-
return whatwgRNG(length).reduce((str, n) => str + byteToHex[n], "").slice(0, length);
|
|
510
|
-
}
|
|
511
568
|
const whitespaceTextNodeRule = {
|
|
512
569
|
deserialize(node) {
|
|
513
570
|
return node.nodeName === "#text" && isWhitespaceTextNode(node) ? {
|
|
@@ -680,6 +737,62 @@ function createHTMLRules(schema, options) {
|
|
|
680
737
|
children: next(el.childNodes)
|
|
681
738
|
} : el.appendChild(el.ownerDocument.createTextNode(` (${href})`)) && next(el.childNodes) : next(el.childNodes);
|
|
682
739
|
}
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
deserialize(el) {
|
|
743
|
+
if (isElement(el) && tagName(el) === "img") {
|
|
744
|
+
const src = el.getAttribute("src") ?? void 0, alt = el.getAttribute("alt") ?? void 0, props = Object.fromEntries(
|
|
745
|
+
Array.from(el.attributes).map((attr) => [attr.name, attr.value])
|
|
746
|
+
), ancestorOfLonelyChild = el?.parentElement?.parentElement?.getAttribute("data-lonely-child"), ancestorOfListItem = el.closest("li") !== null;
|
|
747
|
+
if (ancestorOfLonelyChild && !ancestorOfListItem) {
|
|
748
|
+
const image2 = options.matchers?.image?.({
|
|
749
|
+
context: {
|
|
750
|
+
schema,
|
|
751
|
+
keyGenerator: options.keyGenerator ?? keyGenerator
|
|
752
|
+
},
|
|
753
|
+
props: {
|
|
754
|
+
...props,
|
|
755
|
+
...src ? { src } : {},
|
|
756
|
+
...alt ? { alt } : {}
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
if (image2)
|
|
760
|
+
return {
|
|
761
|
+
_type: "__block",
|
|
762
|
+
block: image2
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const inlineImage = options.matchers?.inlineImage?.({
|
|
766
|
+
context: {
|
|
767
|
+
schema,
|
|
768
|
+
keyGenerator: options.keyGenerator ?? keyGenerator
|
|
769
|
+
},
|
|
770
|
+
props: {
|
|
771
|
+
...props,
|
|
772
|
+
...src ? { src } : {},
|
|
773
|
+
...alt ? { alt } : {}
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
if (inlineImage)
|
|
777
|
+
return inlineImage;
|
|
778
|
+
const image = options.matchers?.image?.({
|
|
779
|
+
context: {
|
|
780
|
+
schema,
|
|
781
|
+
keyGenerator: options.keyGenerator ?? keyGenerator
|
|
782
|
+
},
|
|
783
|
+
props: {
|
|
784
|
+
...props,
|
|
785
|
+
...src ? { src } : {},
|
|
786
|
+
...alt ? { alt } : {}
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
if (image)
|
|
790
|
+
return {
|
|
791
|
+
_type: "__block",
|
|
792
|
+
block: image
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
}
|
|
683
796
|
}
|
|
684
797
|
];
|
|
685
798
|
}
|
|
@@ -757,6 +870,7 @@ function createRules(schema, options) {
|
|
|
757
870
|
];
|
|
758
871
|
}
|
|
759
872
|
class HtmlDeserializer {
|
|
873
|
+
keyGenerator;
|
|
760
874
|
schema;
|
|
761
875
|
rules;
|
|
762
876
|
parseHtml;
|
|
@@ -769,9 +883,10 @@ class HtmlDeserializer {
|
|
|
769
883
|
*/
|
|
770
884
|
constructor(schema, options = {}) {
|
|
771
885
|
const { rules = [], unstable_whitespaceOnPasteMode = "preserve" } = options, standardRules = createRules(schema, {
|
|
772
|
-
keyGenerator: options.keyGenerator
|
|
886
|
+
keyGenerator: options.keyGenerator,
|
|
887
|
+
matchers: options.matchers
|
|
773
888
|
});
|
|
774
|
-
this.schema = schema, this.rules = [...rules, ...standardRules];
|
|
889
|
+
this.schema = schema, this.keyGenerator = options.keyGenerator ?? keyGenerator, this.rules = [...rules, ...standardRules];
|
|
775
890
|
const parseHtml = options.parseHtml || defaultParseHtml();
|
|
776
891
|
this.parseHtml = (html) => preprocess(html, parseHtml, { unstable_whitespaceOnPasteMode }).body;
|
|
777
892
|
}
|
|
@@ -786,8 +901,11 @@ class HtmlDeserializer {
|
|
|
786
901
|
const { parseHtml } = this, fragment = parseHtml(html), children = Array.from(fragment.childNodes), blocks2 = trimWhitespace(
|
|
787
902
|
this.schema,
|
|
788
903
|
flattenNestedBlocks(
|
|
789
|
-
this.schema,
|
|
790
|
-
ensureRootIsBlocks(
|
|
904
|
+
{ schema: this.schema },
|
|
905
|
+
ensureRootIsBlocks(
|
|
906
|
+
this.schema,
|
|
907
|
+
this.deserializeElements(children)
|
|
908
|
+
)
|
|
791
909
|
)
|
|
792
910
|
);
|
|
793
911
|
return this._markDefs.length > 0 && blocks2.filter((block) => isTextBlock(this.schema, block)).forEach((block) => {
|