@portabletext/react-pdf
Version:
Serialize Portable Text to ReactPDF
405 lines (396 loc) • 17.1 kB
JavaScript
import { mergeComponents, PortableText as PortableText$1 } from "@portabletext/react";
import { jsx, jsxs } from "react/jsx-runtime";
import { View, Text, Image, Font } from "@react-pdf/renderer";
import merge from "lodash.merge";
import { flatten } from "flat";
import omitBy from "lodash.omitby";
function mergeStyles(defaultStyles, userStyles) {
return userStyles ? merge({}, defaultStyles, userStyles) : defaultStyles;
}
const rem = (baseFontSizePt = 12, units = 1) => units * baseFontSizePt, normalFontSizing = (baseFontSizePt = 12) => ({
fontSize: rem(baseFontSizePt, 1),
lineHeight: 1.3
}), blockStylesFactory = (baseFontSizePt = 12) => ({
normal: {
marginBottom: rem(baseFontSizePt, 0.25)
},
blockquote: {
marginHorizontal: rem(baseFontSizePt, 1),
marginTop: rem(baseFontSizePt, 0.5),
marginBottom: rem(baseFontSizePt, 1),
paddingLeft: rem(baseFontSizePt, 0.5),
borderLeft: "2px solid gray"
},
h1: {
marginTop: rem(baseFontSizePt, 1.5),
marginBottom: rem(baseFontSizePt, 1)
},
h2: {
marginTop: rem(baseFontSizePt, 1.25),
marginBottom: rem(baseFontSizePt, 0.75)
},
h3: {
marginTop: rem(baseFontSizePt, 1.25),
marginBottom: rem(baseFontSizePt, 0.75)
},
h4: {
marginTop: rem(baseFontSizePt, 1),
marginBottom: rem(baseFontSizePt, 0.5)
},
h5: {
marginTop: rem(baseFontSizePt, 0.75),
marginBottom: rem(baseFontSizePt, 0.5)
},
h6: {
marginTop: rem(baseFontSizePt, 0.75),
marginBottom: rem(baseFontSizePt, 0.5)
}
}), textStylesFactory = (baseFontSizePt = 12) => ({
normal: {
...normalFontSizing(baseFontSizePt)
},
blockquote: {
...normalFontSizing(baseFontSizePt)
},
h1: {
fontSize: rem(baseFontSizePt, 2.5),
lineHeight: 1.1,
fontWeight: 700
},
h2: {
fontSize: rem(baseFontSizePt, 2),
lineHeight: 1.2,
fontWeight: 600
},
h3: {
fontSize: rem(baseFontSizePt, 1.75),
lineHeight: 1.2,
fontWeight: 600
},
h4: {
fontSize: rem(baseFontSizePt, 1.5),
lineHeight: 1.2,
fontWeight: 600
},
h5: {
fontSize: rem(baseFontSizePt, 1.25),
lineHeight: 1.2,
fontWeight: 600
},
h6: {
fontSize: rem(baseFontSizePt, 1),
lineHeight: 1.2,
fontWeight: 600
}
}), marksStylesFactory = (baseFontSizePt = 12) => ({
strong: {
fontWeight: "bold"
},
highlight: {
backgroundColor: "lightyellow"
},
em: {
fontStyle: "italic"
},
link: {
textDecoration: "underline",
color: "blue"
},
underline: {
textDecoration: "underline"
},
"strike-through": {
textDecoration: "line-through"
},
code: {
lineHeight: 1,
backgroundColor: "rgba(27, 31, 35, 0.05)",
fontFamily: "Courier Prime"
},
superscript: {
verticalAlign: "super",
fontSize: rem(baseFontSizePt, 0.75)
},
subscript: {
verticalAlign: "sub",
fontSize: rem(baseFontSizePt, 0.75)
}
}), imageStylesFactory = (baseFontSizePt = 12) => ({
maxWidth: "100%",
height: "auto",
objectFit: "contain",
marginBottom: rem(baseFontSizePt, 0.5)
}), listStylesFactory = (baseFontSizePt = 12) => ({
list: {
...normalFontSizing(baseFontSizePt),
marginTop: rem(baseFontSizePt, 0.5),
marginBottom: rem(baseFontSizePt, 0.5)
},
listDeep: {
...normalFontSizing(baseFontSizePt),
marginTop: 0,
marginBottom: 0
},
listItemWrapper: {
marginVertical: rem(baseFontSizePt, 0.1),
flexDirection: "row"
},
listItemDecorator: {
marginRight: rem(baseFontSizePt, 0.5),
fontFamily: "Dejavu Mono"
},
listItemNumber: {
marginRight: rem(baseFontSizePt, 0.5)
}
}), defaultStylesFactory = (baseFontSizePt = 12) => ({
block: blockStylesFactory(baseFontSizePt),
text: textStylesFactory(baseFontSizePt),
marks: marksStylesFactory(baseFontSizePt),
list: listStylesFactory(baseFontSizePt),
image: imageStylesFactory()
}), toRomanNumeral = (num) => {
const romanNumerals = [
{ value: 50, numeral: "l" },
{ value: 40, numeral: "xl" },
{ value: 10, numeral: "x" },
{ value: 9, numeral: "ix" },
{ value: 5, numeral: "v" },
{ value: 4, numeral: "iv" },
{ value: 1, numeral: "i" }
];
let result = "", remaining = num;
for (const { value, numeral } of romanNumerals)
for (; remaining >= value; )
result += numeral, remaining -= value;
return result;
}, toAlphabetic = (num) => {
let result = "";
for (; num >= 0; )
result = String.fromCharCode(num % 26 + 97) + result, num = Math.floor(num / 26) - 1;
return result;
}, getLevelDecorator = (level, itemIndex) => {
let cycleLevel = 0;
switch (level) {
case 1:
return (itemIndex + 1).toString();
case 2:
return toAlphabetic(itemIndex + 1);
case 3:
return toRomanNumeral(itemIndex + 1);
default:
return cycleLevel = (level - 1) % 3 + 1, getLevelDecorator(cycleLevel, itemIndex);
}
}, defaultListFactory = (styles, baseFontSizePt) => {
const mergedStyles = mergeStyles(defaultStylesFactory(baseFontSizePt), styles);
return (props) => {
const { children, value: list } = props, listStyles = mergedStyles.list || {}, styleKey = list.level && list.level > 1 ? "listDeep" : "list", listStyle = listStyles[styleKey] || {};
return /* @__PURE__ */ jsx(View, { style: listStyle, children }, list._key);
};
}, unicodeBullets = ["\u2022", "\u25E6", "\u25AA\uFE0E"], getDecorator = (level, itemType, itemIndex = 0, styles, baseFontSizePt) => {
if (itemType === "number") {
const decorator = getLevelDecorator(level, itemIndex);
return /* @__PURE__ */ jsxs(Text, { style: { ...styles.list?.listItemNumber, fontSize: 0.9 * baseFontSizePt }, children: [
decorator,
"."
] });
} else {
const bulletCharIndex = (level - 1) % unicodeBullets.length, bulletStyles = bulletCharIndex === 2 ? { ...styles.list?.listItemDecorator, fontSize: 0.8 * baseFontSizePt, paddingTop: baseFontSizePt * 0.05 } : styles.list?.listItemDecorator;
return /* @__PURE__ */ jsx(Text, { style: bulletStyles, children: unicodeBullets[bulletCharIndex] });
}
}, defaultListItemFactory = (styles, baseFontSizePt, itemType) => {
const mergedStyles = mergeStyles(defaultStylesFactory(baseFontSizePt), styles);
return (props) => {
const { children, value: listItem, index } = props, level = listItem.level || 1, paddingLeft = (level - 1) * baseFontSizePt, listItemWrapperStyle = mergedStyles?.list?.listItemWrapper || {}, key = `${listItem._key}__${level}`;
return /* @__PURE__ */ jsx(View, { style: { ...listItemWrapperStyle, paddingLeft }, children: /* @__PURE__ */ jsxs(View, { style: { display: "flex", flexDirection: "row", alignItems: "flex-start" }, children: [
getDecorator(level, itemType, level === 2 ? index - 1 : index, mergedStyles, baseFontSizePt),
/* @__PURE__ */ jsx(Text, { children })
] }) }, key);
};
}, hardBreak = () => /* @__PURE__ */ jsx(Text, { children: `
` }), defaultUnknownMarkFactory = () => (props) => {
const { children, value: mark, markType } = props;
return console.warn(`Unknown mark type "${markType || "undefined"}", please specify a component for it in the \`components.marks\` prop`), /* @__PURE__ */ jsx(Text, { children }, mark?._key);
}, defaultUnknownTypeFactory = () => (props) => {
const { value } = props;
return console.warn(`Unknown block type "${value._type || "undefined"}", specify a component for it in the \`components.types\` prop`), /* @__PURE__ */ jsx(View, {});
}, defaultUnknownBlockStyleFactory = (styles, baseFontSizePt) => (props) => {
const { value: block } = props;
return console.warn(`Unknown block style "${block.style || "undefined"}", please specify a component for it in the \`components.block\` prop`), props.value.style = "normal", defaultBlockFactory(styles, baseFontSizePt)(props);
}, defaultUnknownListFactory = () => (props) => {
const { children, value: block } = props;
return console.warn(`Unknown list style "${block.listItem || "undefined"}", please specify a component for it in the \`components.list\` prop`), /* @__PURE__ */ jsx(View, { children }, block._key);
}, defaultUnknownListItemFactory = (baseFontSizePt) => (props) => {
const { value: block } = props;
return console.warn(`Unknown list item style "${block?.listItem || "undefined"}", please specify a component for it in the \`components.list\` prop`), defaultListItemFactory({}, baseFontSizePt, "bullet")(props);
}, defaultBlockFactory = (styles, baseFontSizePt) => {
const mergedStyles = mergeStyles(defaultStylesFactory(baseFontSizePt), styles);
return (props) => {
const { children, value: block } = props, styleKey = block.style || "normal", blockStyles = mergedStyles.block || {}, textStyles = mergedStyles.text || {};
return block?.children?.length === 1 && block?.children?.[0]?.text === "" ? hardBreak() : /* @__PURE__ */ jsx(View, { style: blockStyles[styleKey], children: /* @__PURE__ */ jsx(Text, { style: textStyles[styleKey], children }) }, block._key);
};
}, defaultImageFactory = (styles, baseFontSizePt) => {
const mergedStyles = mergeStyles(defaultStylesFactory(baseFontSizePt), styles);
return (props) => {
const { value } = props, image = value?.url || value?.src || "";
return /* @__PURE__ */ jsx(Image, { src: image, style: mergedStyles.image }, value?._key);
};
}, defaultPageBreakFactory = () => () => /* @__PURE__ */ jsx(View, { break: !0 }), defaultMarksFactory = (styles, baseFontSizePt, itemType) => {
const mergedStyles = mergeStyles(defaultStylesFactory(baseFontSizePt), styles);
return (props) => {
const { children } = props, marksStyles = mergedStyles?.marks || {};
return /* @__PURE__ */ jsx(Text, { style: marksStyles[itemType], children });
};
}, generateStyledDefaultComponentsMap = (styles, baseFontSizePt) => ({
types: {
break: defaultPageBreakFactory(),
image: defaultImageFactory(styles, baseFontSizePt)
},
block: {
normal: defaultBlockFactory(styles, baseFontSizePt),
blockquote: defaultBlockFactory(styles, baseFontSizePt),
h1: defaultBlockFactory(styles, baseFontSizePt),
h2: defaultBlockFactory(styles, baseFontSizePt),
h3: defaultBlockFactory(styles, baseFontSizePt),
h4: defaultBlockFactory(styles, baseFontSizePt),
h5: defaultBlockFactory(styles, baseFontSizePt),
h6: defaultBlockFactory(styles, baseFontSizePt)
},
marks: {
em: defaultMarksFactory(styles, baseFontSizePt, "em"),
strong: defaultMarksFactory(styles, baseFontSizePt, "strong"),
code: defaultMarksFactory(styles, baseFontSizePt, "code"),
underline: defaultMarksFactory(styles, baseFontSizePt, "underline"),
"strike-through": defaultMarksFactory(styles, baseFontSizePt, "strike-through"),
superscript: defaultMarksFactory(styles, baseFontSizePt, "superscript"),
subscript: defaultMarksFactory(styles, baseFontSizePt, "subscript"),
link: defaultMarksFactory(styles, baseFontSizePt, "link"),
highlight: defaultMarksFactory(styles, baseFontSizePt, "highlight")
},
list: {
bullet: defaultListFactory(styles, baseFontSizePt),
number: defaultListFactory(styles, baseFontSizePt)
},
listItem: {
bullet: defaultListItemFactory(styles, baseFontSizePt, "bullet"),
number: defaultListItemFactory(styles, baseFontSizePt, "number")
},
hardBreak,
unknownType: defaultUnknownTypeFactory(),
unknownMark: defaultUnknownMarkFactory(),
unknownList: defaultUnknownListFactory(),
unknownListItem: defaultUnknownListItemFactory(baseFontSizePt),
unknownBlockStyle: defaultUnknownBlockStyleFactory(styles, baseFontSizePt)
}), mergeAndStyleComponents = (components, styles, baseFontSizePt) => {
const styledDefaultComponentsMap = generateStyledDefaultComponentsMap(styles, baseFontSizePt);
return components ? mergeComponents(styledDefaultComponentsMap, components) : styledDefaultComponentsMap;
};
Font.register({
family: "Source Sans Pro",
fonts: [
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/source-sans-pro@latest/latin-400-normal.ttf",
fontWeight: "normal",
fontStyle: "normal"
},
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/source-sans-pro@latest/latin-400-italic.ttf",
fontWeight: "normal",
fontStyle: "italic"
},
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/source-sans-pro@latest/latin-700-normal.ttf",
fontWeight: "bold",
fontStyle: "normal"
},
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/source-sans-pro@latest/latin-700-italic.ttf",
fontWeight: "bold",
fontStyle: "italic"
}
]
});
Font.register({
family: "Courier Prime",
fonts: [
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/courier-prime@latest/latin-400-normal.ttf",
fontWeight: "normal",
fontStyle: "normal"
},
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/courier-prime@latest/latin-400-italic.ttf",
fontWeight: "normal",
fontStyle: "italic"
},
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/courier-prime@latest/latin-700-normal.ttf",
fontWeight: "bold",
fontStyle: "normal"
},
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/courier-prime@latest/latin-700-italic.ttf",
fontWeight: "bold",
fontStyle: "italic"
}
]
});
Font.register({
family: "Dejavu Mono",
fonts: [
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/dejavu-mono@latest/latin-400-normal.ttf",
fontWeight: "normal",
fontStyle: "normal"
},
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/dejavu-mono@latest/latin-400-italic.ttf",
fontWeight: "normal",
fontStyle: "italic"
},
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/dejavu-mono@latest/latin-700-normal.ttf",
fontWeight: "bold",
fontStyle: "normal"
},
{
src: "https://cdn.jsdelivr.net/fontsource/fonts/dejavu-mono@latest/latin-700-italic.ttf",
fontWeight: "bold",
fontStyle: "italic"
}
]
});
const isNil = (value) => value == null, checkPropsOverlap = (props) => {
const { components = {}, defaultComponentStyles = {} } = props;
if (components && defaultComponentStyles) {
const defaultStyleKeys = Object.keys(defaultComponentStyles), typeKeys = Object.keys(components?.types || {}), overlappingTypeKeys = defaultStyleKeys.filter((key) => typeKeys.indexOf(key) !== -1), defaultComponentStylesPaths = Object.keys(omitBy(flatten(defaultComponentStyles, { maxDepth: 2 }) || {}, isNil)), componentPaths = Object.keys(omitBy(flatten(components, { maxDepth: 2 }) || {}, isNil)), overlappingPaths = defaultComponentStylesPaths.filter((key) => componentPaths.indexOf(key) !== -1);
if (overlappingPaths?.length > 0) {
const errorMessage = `
OVERLAPPING PROPS: Paths with a component defined in "components" and paths with a style defined in "defaultComponentStyles" may not overlap.
You've specified both a component and a style for the following path(s) in those objects: ${overlappingPaths.map((elem) => `"${elem}"`).join(", ")}.
You may specify both props, as long as there are not matching paths in the two objects resulting in both a component and a style being defined for that path (would lead to confusing behavior).
For example, you MAY specify a value for "block.h1" in one of those prop objects and a value for "block.h2" in the other, but you may NOT specify both a component and a style for "block.h1".`;
throw console.error(errorMessage), new Error(errorMessage);
} else if (overlappingTypeKeys?.length > 0) {
const errorMessage = `
OVERLAPPING PROPS: Keys with a component defined in "components.types" and keys with a style defined in "defaultComponentStyles" may not overlap.
You've specified both a component and a style for the following key(s) in those objects: ${overlappingTypeKeys.map((elem) => `"${elem}"`).join(", ")}.
You may specify both props, as long there are not matching keys in "component.types" and "defaultComponentStyles" resulting in both a component and a style being defined for that same key (would lead to confusing behavior).
For example, you MAY specify a component for "components.types.block" and a style for "defaultComponentStyles.list", but you may NOT specify an value for both "components.types.block" and "defaultComponentStyles.block".`;
throw console.error(errorMessage), new Error(errorMessage);
}
}
};
function PortableText(props) {
const { baseFontSizePt = 12, defaultComponentStyles = {}, components, ...portableTextProps } = props, mergedAndStyledComponents = mergeAndStyleComponents(components, defaultComponentStyles, baseFontSizePt);
return checkPropsOverlap(props), /* @__PURE__ */ jsx(PortableText$1, { listNestingMode: "direct", ...portableTextProps, components: mergedAndStyledComponents });
}
var types = /* @__PURE__ */ Object.freeze({
__proto__: null
});
export {
PortableText,
generateStyledDefaultComponentsMap,
mergeAndStyleComponents,
types
};
//# sourceMappingURL=index.js.map