From 1018b1a11b8ebecb64f2832ca0a443eeea846ecc Mon Sep 17 00:00:00 2001 From: Albina Starykova Date: Mon, 17 Jun 2024 14:41:50 +0300 Subject: [PATCH] Add content metrics board --- client/src/entrypoints/admin/preview-panel.js | 3 + client/src/includes/contentMetrics.test.ts | 41 +++++++++ client/src/includes/contentMetrics.ts | 86 +++++++++++++++++++ .../shared/side_panels/checks.html | 35 ++++++-- 4 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 client/src/includes/contentMetrics.test.ts create mode 100644 client/src/includes/contentMetrics.ts diff --git a/client/src/entrypoints/admin/preview-panel.js b/client/src/entrypoints/admin/preview-panel.js index 353ac846f575..c8855032450f 100644 --- a/client/src/entrypoints/admin/preview-panel.js +++ b/client/src/entrypoints/admin/preview-panel.js @@ -3,6 +3,7 @@ import { getAxeConfiguration, renderA11yResults, } from '../../includes/a11y-result'; +import { runContentCheck } from '../../includes/contentMetrics'; import { WAGTAIL_CONFIG } from '../../config/wagtailConfig'; import { debounce } from '../../utils/debounce'; import { gettext } from '../../utils/gettext'; @@ -206,6 +207,8 @@ function initPreview() { // Remove the load event listener so it doesn't fire when switching modes newIframe.removeEventListener('load', handleLoad); + runContentCheck(); + const onClickSelector = () => newTabButton.click(); runAccessibilityChecks(onClickSelector); }; diff --git a/client/src/includes/contentMetrics.test.ts b/client/src/includes/contentMetrics.test.ts new file mode 100644 index 000000000000..9330ae068bcb --- /dev/null +++ b/client/src/includes/contentMetrics.test.ts @@ -0,0 +1,41 @@ +import { getContentMetrics } from './contentMetrics'; + +describe('getContentMetrics', () => { + it('should return correct wordCount and readingTime using Intl.Segmenter', () => { + const result = getContentMetrics('en-US', 'This is a test sentence.'); + expect(result.wordCount).toBe(5); + expect(result.readingTime).toBe(0); + }); + + it('should handle empty text', () => { + const result = getContentMetrics('en-US', ''); + expect(result.wordCount).toBe(0); + expect(result.readingTime).toBe(0); + }); + + it('should handle text with punctuation correctly', () => { + const text = `This is a longer text to test the word count and reading time calculation! + Bread is a staple food prepared from a dough of flour and water; usually by baking. + Throughout recorded history it has been popular around the world and is one of the + oldest artificial foods: having been of importance since the dawn of agriculture. + Proportions of types of flour and other ingredients vary widely? as do modes of preparation. + As a result, types, shapes, sizes, and textures of breads differ around the world. + Bread may be leavened by processes such as reliance on naturally occurring sourdough + microbes, chemicals, industrially produced yeast, or high-pressure aeration... + Some breads are baked before they have a chance to rise, often for traditional + or religious reasons. Inclusions like fruits; nuts, and fats are sometimes added. + Commercial bread typically includes additives to enhance flavor, texture, color, + longevity, and production efficiency! + `; + const result = getContentMetrics('en-US', text); + expect(result.wordCount).toBe(147); + expect(result.readingTime).toBe(1); + }); + + it('should return integers for wordCount and readingTime', () => { + const text = 'Yet another text'; + const result = getContentMetrics('en-US', text); + expect(Number.isInteger(result.wordCount)).toBe(true); + expect(Number.isInteger(result.readingTime)).toBe(true); + }); +}); diff --git a/client/src/includes/contentMetrics.ts b/client/src/includes/contentMetrics.ts new file mode 100644 index 000000000000..adf0f8b3630c --- /dev/null +++ b/client/src/includes/contentMetrics.ts @@ -0,0 +1,86 @@ +interface ContentMetrics { + wordCount: number; + readingTime: number; +} + +interface SegmentData { + segment: string; + isWordLike?: boolean | undefined; +} + +export const getContentMetrics = ( + lang: string, + text: string, +): ContentMetrics => { + let wordCount = 0; + + if (typeof Intl.Segmenter === 'function') { + const segmenter = new Intl.Segmenter(lang, { granularity: 'word' }); + const segments: SegmentData[] = Array.from(segmenter.segment(text)); + wordCount = segments.reduce( + (count, segment) => (segment.isWordLike ? count + 1 : count), + 0, + ); + } else { + // Fallback to regex if Intl.Segmenter is not supported + wordCount = + text + .trim() + .replace(/['";:,.?¿\-!¡]+/g, '') + .match(/\S+/g)?.length || 0; + } + + // Silent-reading adults average 238 words per minute + const readingTime = Math.round(wordCount / 238); + + return { + wordCount, + readingTime, + }; +}; + +const renderContentMetrics = ({ wordCount, readingTime }: ContentMetrics) => { + const wordCountContainer = document.querySelector( + '[data-content-word-count]', + ); + const readingTimeContainer = document.querySelector( + '[data-content-reading-time]', + ); + const readingTimeSingleUnit = document.querySelector( + '[data-content-reading-time-single]', + ); + const readingTimeUnitPluralUnit = document.querySelector( + '[data-content-reading-time-plural]', + ); + + if ( + !wordCountContainer || + !readingTimeContainer || + !readingTimeSingleUnit || + !readingTimeUnitPluralUnit + ) + return; + + if (readingTime === 1) { + readingTimeSingleUnit.hidden = false; + readingTimeUnitPluralUnit.hidden = true; + } + wordCountContainer.textContent = wordCount.toString(); + readingTimeContainer.textContent = readingTime.toString(); +}; + +export const runContentCheck = () => { + const iframe = document.querySelector( + '[data-preview-iframe]', + ); + const iframeDocument = + iframe?.contentDocument || iframe?.contentWindow?.document; + const text = iframeDocument?.querySelector('main')?.innerText; + if (!iframe || !iframeDocument || !text) { + return; + } + const lang = iframeDocument.documentElement.lang || 'en-US'; + const contentMetrics = getContentMetrics(lang, text); + + renderContentMetrics(contentMetrics); +}; diff --git a/wagtail/admin/templates/wagtailadmin/shared/side_panels/checks.html b/wagtail/admin/templates/wagtailadmin/shared/side_panels/checks.html index e8f10427ef8c..a47ee8c24361 100644 --- a/wagtail/admin/templates/wagtailadmin/shared/side_panels/checks.html +++ b/wagtail/admin/templates/wagtailadmin/shared/side_panels/checks.html @@ -1,5 +1,4 @@ {% load i18n wagtailadmin_tags %} - - -
-

{% trans 'Issues found' %}0

-
+
+
+

+ {% trans 'Content metrics' %} +

+
+
+

{% trans 'Words' %}

+

0

+
+
+

{% trans 'Reading time' %}

+

+ 0 + + {% trans " mins" %} +

+
+
+
+
+

+ {% trans 'Issues found' %}0 +

+
+
{{ axe_configuration|json_script:"accessibility-axe-configuration" }}