• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kiva / ui / 19147367510

06 Nov 2025 07:26PM UTC coverage: 91.443% (+41.5%) from 49.902%
19147367510

push

github

emuvente
test: refactor category-row-arrows-visible-mixin test with runner method

3722 of 3979 branches covered (93.54%)

Branch coverage included in aggregate %.

18923 of 20785 relevant lines covered (91.04%)

78.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.93
/src/util/contentful/richTextRenderer.js
1
/*
1✔
2
 * Custom renderer for Contentful Rich Text content
1✔
3
 * Has the ability to return html for embedded assets
1✔
4
 * and entries inside of that rich text content.
1✔
5
 * Docs: https://github.com/contentful/rich-text/tree/master/packages/rich-text-html-renderer#usage
1✔
6
 */
1✔
7

8
import {
1✔
9
        formatResponsiveImageSet, responsiveImageSetSourceSets, formatContentTypes
10
} from '#src/util/contentfulUtils';
11
import { BLOCKS, INLINES } from '@contentful/rich-text-types';
1✔
12
import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
1✔
13

14
/**
1✔
15
 * Returns a string representation of a value
1✔
16
 * which can be inserted in an html attribute in quotes.
1✔
17
 * Call stringify, then replaces single quotes with escaped
1✔
18
 * then replaces double quotes with single quotes
1✔
19
 *
1✔
20
 * @param {string|object|array} value any value to convert to string
1✔
21
 * @returns {string} string representation
1✔
22
 */
1✔
23
function htmlSafeStringify(value) {
3✔
24
        return JSON.stringify(value).replace(/'/g, '\\\'').replace(/"/g, '\'');
3✔
25
}
3✔
26

27
/**
1✔
28
 * Contentful rich text fields add a trailing empty <p> tag, these should be removed
1✔
29
 *
1✔
30
 * @param {object} contentful RTF object containing RTF nodes
1✔
31
 * @returns {object} RTF object containing RTF nodes without trailing empty <p> tag
1✔
32
 */
1✔
33
export function removeTrailingParagraphTag(content) {
1✔
34
        if (content) {
10✔
35
                // Remove empty <p> inserted by contentful
10✔
36
                const innerContent = content?.content;
10✔
37
                const lastRTFElement = innerContent[innerContent.length - 1];
10✔
38

39
                const contentWithoutTrailingEmptyParagraph = { ...content };
10✔
40

41
                // If last item is an empty paragraph, which contains an empty text node, remove it
10✔
42
                if (lastRTFElement.nodeType === 'paragraph'
10✔
43
                        && Object.keys(lastRTFElement.data).length === 0
10✔
44
                        && lastRTFElement.content.length === 1
10✔
45
                        && lastRTFElement.content?.[0]?.value === ''
10✔
46
                        && lastRTFElement.content?.[0]?.nodeType === 'text') {
10✔
47
                        contentWithoutTrailingEmptyParagraph.content = contentWithoutTrailingEmptyParagraph.content.slice(0, -1);
1✔
48
                }
1✔
49
                return contentWithoutTrailingEmptyParagraph;
10✔
50
        }
10!
51
        return content;
×
52
}
×
53

54
/**
1✔
55
 * Returns html string from rich text nodes
1✔
56
 *
1✔
57
 * @param {object} content Content of a contentful rich text field
1✔
58
 * @returns {string} String of html to render
1✔
59
 */
1✔
60
export function richTextRenderer(content) {
1✔
61
        /**
9✔
62
         * Returns html string to render a contentful asset as a vue component
9✔
63
         *
9✔
64
         * @param {object} contentfulAssetObject Content of a contentful asset object
9✔
65
         * @returns {string} String of html to render
9✔
66
         */
9✔
67
        const assetRenderer = contentfulAssetObject => {
9✔
68
                const isVideo = contentfulAssetObject?.file?.contentType.includes('video');
3✔
69
                const isImage = contentfulAssetObject?.file?.contentType.includes('image');
3✔
70
                if (isImage) {
3✔
71
                        return `
1✔
72
                        <kv-contentful-img
1✔
73
                                class="tw-whitespace-normal"
1✔
74
                                contentful-src="${encodeURI(contentfulAssetObject?.file?.url)}"
1✔
75
                                alt="${contentfulAssetObject?.description}"
1✔
76
                                height="${contentfulAssetObject?.file?.details?.image?.height}"
1✔
77
                                width="${contentfulAssetObject?.file?.details?.image?.width}"/>
1✔
78
                        `;
1✔
79
                } if (isVideo) {
3✔
80
                // video media
1✔
81
                        return `
1✔
82
                        <video
1✔
83
                                :src="${encodeURI(contentfulAssetObject?.file?.url)}"
1✔
84
                                autoplay
1✔
85
                                loop
1✔
86
                                muted
1✔
87
                                playsinline
1✔
88
                        ></video>`;
1✔
89
                }
1✔
90
                return '';
1✔
91
        };
9✔
92

93
        /**
9✔
94
         * Returns html string to render a contentful entry, possibly as a vue component
9✔
95
         *
9✔
96
         * @param {object} contentfulEntryNode Contentful rich text node for an entry
9✔
97
         * @returns {string} String of html to render
9✔
98
         */
9✔
99
        const entryRenderer = contentfulEntryNode => {
9✔
100
                const entryContentTypeId = contentfulEntryNode?.data?.target?.sys?.contentType?.sys?.id;
5✔
101
                const entryContent = contentfulEntryNode?.data?.target;
5✔
102

103
                const isRichTextContent = entryContentTypeId === 'richTextContent';
5✔
104
                const isButton = entryContentTypeId === 'button';
5✔
105
                const isResponsiveImageSet = entryContentTypeId === 'responsiveImageSet';
5✔
106
                const isFAQ = entryContent?.fields?.type === 'frequentlyAskedQuestions';
5✔
107

108
                if (isRichTextContent) {
5✔
109
                        const richTextHTML = richTextRenderer(entryContent?.fields?.richText);
1✔
110
                        return `<div>${richTextHTML}</div>`;
1✔
111
                }
1✔
112
                if (isButton) {
5✔
113
                        // The content prop expects an object, but in this context
1✔
114
                        // only passing in a string representation of an object will work
1✔
115
                        // We must stringify the object, then replace the quotes
1✔
116

117
                        const buttonObjectAsString = htmlSafeStringify(entryContent?.fields);
1✔
118
                        return `<button-wrapper class="tw-whitespace-normal" :content="${buttonObjectAsString}" />`;
1✔
119
                }
1✔
120
                if (isResponsiveImageSet) {
5✔
121
                        const formattedResponsiveImageSet = formatResponsiveImageSet(entryContent);
1✔
122
                        const sourceSets = responsiveImageSetSourceSets(formattedResponsiveImageSet);
1✔
123
                        const sourceSetArrayAsString = htmlSafeStringify(sourceSets);
1✔
124
                        return `<kv-contentful-img
1✔
125
                                                class="tw-whitespace-normal"
1✔
126
                                                contentful-src="${encodeURI(sourceSets[0].url)}"
1✔
127
                                                width="${sourceSets[0].width}"
1✔
128
                                                height="${sourceSets[0].height}"
1✔
129
                                                fallback-format="jpg"
1✔
130
                                                alt="${formattedResponsiveImageSet?.description}"
1✔
131
                                                :source-sizes="${sourceSetArrayAsString}" />`;
1✔
132
                }
1✔
133
                if (isFAQ) {
5✔
134
                        const questions = formatContentTypes(entryContent?.fields?.contents)
1✔
135
                                .filter(entry => entry.contentType === 'richTextContent');
1✔
136
                        return `<kv-frequently-asked-questions
1✔
137
                                                :questions="${htmlSafeStringify(questions)}"
1✔
138
                                        />`;
1✔
139
                }
1✔
140
                return '';
1✔
141
        };
9✔
142

143
        const options = {
9✔
144
                renderNode: {
9✔
145
                        [INLINES.EMBEDDED_ENTRY]: node => {
9✔
146
                                return entryRenderer(node);
×
147
                        },
9✔
148
                        [BLOCKS.EMBEDDED_ENTRY]: node => {
9✔
149
                                return entryRenderer(node);
5✔
150
                        },
9✔
151
                        [BLOCKS.EMBEDDED_ASSET]: node => {
9✔
152
                                return assetRenderer(node?.data?.target?.fields);
3✔
153
                        },
9✔
154
                }
9✔
155
        };
9✔
156

157
        const contentWithoutTrailingEmptyParagraph = removeTrailingParagraphTag(content);
9✔
158

159
        return documentToHtmlString(contentWithoutTrailingEmptyParagraph, options);
9✔
160
}
9✔
161

162
/**
1✔
163
 * Adds target="_blank" to links so they open in a new tab
1✔
164
 *
1✔
165
 * @param {String} bodyCopy String containing html of contentful entry
1✔
166
 * @param {Object} pageSettings Object containing global page settings of
1✔
167
 * the page that the contentful entry is from
1✔
168
 * @returns {void}
1✔
169
 */
1✔
170
export function addBlankTargetToExternalLinks(bodyCopy, pageSettings) {
1✔
171
        // make sure all partner content links open externally
5✔
172
        if (bodyCopy && pageSettings?.enableBlankTargetLinks) {
5✔
173
                const links = bodyCopy.querySelectorAll('a');
2✔
174
                if (links.length > 0) {
2✔
175
                        Array.prototype.forEach.call(links, link => {
2✔
176
                                link.target = '_blank';// eslint-disable-line no-param-reassign
4✔
177
                        });
2✔
178
                }
2✔
179
        }
2✔
180
}
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc