Skip to content

Commit

Permalink
Add content metrics board
Browse files Browse the repository at this point in the history
  • Loading branch information
albinazs committed Jun 17, 2024
1 parent a7798c1 commit 1018b1a
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 6 deletions.
3 changes: 3 additions & 0 deletions client/src/entrypoints/admin/preview-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
};
Expand Down
41 changes: 41 additions & 0 deletions client/src/includes/contentMetrics.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
86 changes: 86 additions & 0 deletions client/src/includes/contentMetrics.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>(
'[data-content-word-count]',
);
const readingTimeContainer = document.querySelector<HTMLElement>(
'[data-content-reading-time]',
);
const readingTimeSingleUnit = document.querySelector<HTMLElement>(
'[data-content-reading-time-single]',
);
const readingTimeUnitPluralUnit = document.querySelector<HTMLElement>(
'[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<HTMLIFrameElement>(
'[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);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{% load i18n wagtailadmin_tags %}

<template id="w-a11y-result-row-template">
<div class="w-a11y-result__row" data-a11y-result-row>
<h3 class="w-a11y-result__header">
Expand All @@ -10,14 +9,38 @@ <h3 class="w-a11y-result__header">
</div>
</template>
<template id="w-a11y-result-selector-template">
<button class="w-a11y-result__selector" data-a11y-result-selector type="button">
<button class="w-a11y-result__selector"
data-a11y-result-selector
type="button">
{% icon name="link-external" classname="w-a11y-result__icon" %}
<span data-a11y-result-selector-text></span>
</button>
</template>

<div class="w-mt-12">
<h2 class="w-flex w-items-center w-gap-2"><span>{% trans 'Issues found' %}</span><span class="w-a11y-result__count" data-a11y-result-count>0</span></h2>
<div data-checks-panel></div>
<div class="w-divide-y w-divide-border-furniture w-py-6 w-pl-2 lg:w-pl-8">
<div>
<h2 class="w-my-5 w-text-16 w-font-bold w-text-text-label">
<span>{% trans 'Content metrics' %}</span>
</h2>
<div class="w-flex w-gap-10">
<div>
<h3 class="w-my-2 w-text-14 w-text-text-placeholder">{% trans 'Words' %}</h3>
<p class="w-font-semibold w-text-text-label" data-content-word-count>0</p>
</div>
<div>
<h3 class="w-my-2 w-text-14 w-text-text-placeholder">{% trans 'Reading time' %}</h3>
<p class="w-font-semibold w-text-text-label">
<span data-content-reading-time>0</span>
<span hidden data-content-reading-time-single>{% trans " min" %}</span>
<span data-content-reading-time-plural>{% trans " mins" %}</span>
</p>
</div>
</div>
</div>
<div>
<h2 class="w-flex w-items-center w-gap-2 w-my-5 w-text-16 w-font-bold w-text-text-label">
<span>{% trans 'Issues found' %}</span><span class="w-a11y-result__count" data-a11y-result-count>0</span>
</h2>
<div class="w-flex w-flex-col w-gap-2.5" data-checks-panel></div>
</div>
</div>
{{ axe_configuration|json_script:"accessibility-axe-configuration" }}

0 comments on commit 1018b1a

Please sign in to comment.