forked from wagtail/wagtail
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1537889
commit 6e9ac2f
Showing
9 changed files
with
273 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { getWordCount, getReadingTime } from './contentMetrics'; | ||
|
||
describe.each` | ||
text | lang | wordCount | ||
${'¿Donde esta la biblioteca?'} | ${'es'} | ${4} | ||
${"It's lots. Of; Punctuation"} | ${'en'} | ${4} | ||
${'האהבה היא אוקיינוס שאין לו התחלה ואין לו סוף.'} | ${'he'} | ${9} | ||
${'元気です、ありがとう。あなたは?'} | ${'zh'} | ${5} | ||
${'Dit is een testzin in het Nederlands.'} | ${'nl'} | ${7} | ||
${'Je suis content de te voir!'} | ${'fr'} | ${6} | ||
${'Ich liebe dich!'} | ${'de'} | ${3} | ||
${'Mi piace molto questo libro.'} | ${'it'} | ${5} | ||
${'저는 오늘 날씨가 좋아요.'} | ${'ko'} | ${4} | ||
${'Unknown language code still works'} | ${'invalid'} | ${5} | ||
`('getWordCount', ({ text, lang, wordCount }) => { | ||
test(`correctly counts words in '${text}' for language '${lang}'`, () => { | ||
expect(getWordCount(lang, text)).toBe(wordCount); | ||
}); | ||
}); | ||
|
||
describe.each` | ||
lang | wordCount | readingTime | ||
${'es'} | ${1000} | ${4} | ||
${'fr'} | ${1000} | ${5} | ||
${'ar'} | ${360} | ${2} | ||
${'it'} | ${360} | ${1} | ||
${'en'} | ${238} | ${1} | ||
${'en-us'} | ${238} | ${1} | ||
${'he'} | ${224} | ${1} | ||
${'zh'} | ${520} | ${2} | ||
${'zh-Hans'} | ${520} | ${2} | ||
${'nl'} | ${320} | ${1} | ||
${'ko'} | ${50} | ${0} | ||
${'invalid'} | ${1000} | ${4} | ||
${''} | ${1000} | ${4} | ||
`('getReadingTime', ({ lang, wordCount, readingTime }) => { | ||
test(`calculates reading time for '${wordCount}' words in language '${lang}'`, () => { | ||
expect(getReadingTime(lang, wordCount)).toBe(readingTime); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import axe from 'axe-core'; | ||
import { ngettext } from '../utils/gettext'; | ||
|
||
export const getWordCount = (lang: string, text: string): number => { | ||
// Firefox ESR doesn’t have support for Intl.Segmenter yet. | ||
if (typeof Intl.Segmenter === 'undefined') { | ||
return 0; | ||
} | ||
|
||
const segmenter = new Intl.Segmenter(lang, { granularity: 'word' }); | ||
const segments: Intl.SegmentData[] = Array.from(segmenter.segment(text)); | ||
const wordCount = segments.reduce( | ||
(count, segment) => (segment.isWordLike ? count + 1 : count), | ||
0, | ||
); | ||
|
||
return wordCount; | ||
}; | ||
|
||
/* | ||
Language-specific reading speeds according to a meta-analysis of 190 studies on reading rates. | ||
Study preprint: https://osf.io/preprints/psyarxiv/xynwg/ | ||
DOI: https://doi.org/10.1016/j.jml.2019.104047 | ||
*/ | ||
const readingSpeeds = { | ||
ar: 181, // Arabic | ||
zh: 260, // Chinese | ||
nl: 228, // Dutch | ||
en: 238, // English | ||
fi: 195, // Finnish | ||
fr: 214, // French | ||
de: 260, // German | ||
he: 224, // Hebrew | ||
it: 285, // Italian | ||
ko: 226, // Korean | ||
es: 278, // Spanish | ||
sv: 218, // Swedish | ||
}; | ||
|
||
export const getReadingTime = (lang: string, wordCount: number): number => { | ||
const locale = lang.split('-')[0]; | ||
// Fallback to English reading speed if the locale is not found | ||
const readingSpeed = readingSpeeds[locale] || readingSpeeds.en; | ||
const readingTime = Math.round(wordCount / readingSpeed); | ||
|
||
return readingTime; | ||
}; | ||
|
||
interface ContentMetricsOptions { | ||
targetElement: string; | ||
} | ||
|
||
interface ContentMetrics { | ||
wordCount: number; | ||
readingTime: number; | ||
} | ||
|
||
export const contentMetricsPluginInstance = { | ||
id: 'metrics', | ||
getMetrics( | ||
options: ContentMetricsOptions, | ||
done: (metrics: ContentMetrics) => void, | ||
) { | ||
const main = document.querySelector<HTMLElement>(options.targetElement); | ||
const text = main?.innerText || ''; | ||
const lang = document.documentElement.lang || 'en'; | ||
const wordCount = getWordCount(lang, text); | ||
const readingTime = getReadingTime(lang, wordCount); | ||
done({ | ||
wordCount, | ||
readingTime, | ||
}); | ||
}, | ||
}; | ||
|
||
/** | ||
* Calls the `getMetrics` method in the `metrics` plugin instance of the `wagtailPreview` registry. | ||
* Wrapped in a promise so we can use async/await syntax instead of callbacks | ||
*/ | ||
export const getPreviewContentMetrics = ( | ||
options: ContentMetricsOptions, | ||
): Promise<ContentMetrics> => | ||
new Promise((resolve) => { | ||
axe.plugins.wagtailPreview.run( | ||
'metrics', | ||
'getMetrics', | ||
options, | ||
(metrics: ContentMetrics) => { | ||
resolve(metrics); | ||
}, | ||
); | ||
}); | ||
|
||
export const renderContentMetrics = ({ | ||
wordCount, | ||
readingTime, | ||
}: ContentMetrics) => { | ||
// Skip updates if word count isn’t set. | ||
if (!wordCount) { | ||
return; | ||
} | ||
|
||
const wordCountContainer = document.querySelector<HTMLElement>( | ||
'[data-content-word-count]', | ||
); | ||
const readingTimeContainer = document.querySelector<HTMLElement>( | ||
'[data-content-reading-time]', | ||
); | ||
|
||
if (!wordCountContainer || !readingTimeContainer) return; | ||
|
||
wordCountContainer.textContent = wordCount.toString(); | ||
readingTimeContainer.textContent = ngettext( | ||
'%(num)s min', | ||
'%(num)s mins', | ||
readingTime, | ||
).replace('%(num)s', `${readingTime}`); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import axe, { AxePlugin } from 'axe-core'; | ||
|
||
/** | ||
* Axe plugin registry for interaction between the page editor and the live preview. | ||
* Compared to other aspects of Axe and other plugins, | ||
* - The parent frame only triggers execution of the plugin’s logic in the one frame. | ||
* - The preview frame only executes the plugin’s logic, it doesn’t go through its own frames. | ||
* See https://github.com/dequelabs/axe-core/blob/master/doc/plugins.md. | ||
*/ | ||
export const wagtailPreviewPlugin: AxePlugin = { | ||
id: 'wagtailPreview', | ||
run(id, action, options, callback) { | ||
// Outside the preview frame, we need to send the command to the preview iframe. | ||
const preview = document.querySelector<HTMLIFrameElement>( | ||
'[data-preview-iframe]', | ||
); | ||
|
||
if (preview) { | ||
// @ts-expect-error Not declared in the official Axe Utils API. | ||
axe.utils.sendCommandToFrame( | ||
preview, | ||
{ | ||
command: 'run-wagtailPreview', | ||
parameter: id, | ||
action: action, | ||
options: options, | ||
}, | ||
(results) => { | ||
// Pass the results from the preview iframe to the callback. | ||
callback(results); | ||
}, | ||
); | ||
} else { | ||
// Inside the preview frame, only call the expected plugin instance method. | ||
// eslint-disable-next-line no-underscore-dangle | ||
const pluginInstance = this._registry[id]; | ||
pluginInstance[action].call(pluginInstance, options, callback); | ||
} | ||
}, | ||
commands: [ | ||
{ | ||
id: 'run-wagtailPreview', | ||
callback(data, callback) { | ||
return axe.plugins.wagtailPreview.run( | ||
data.parameter, | ||
data.action, | ||
data.options, | ||
callback, | ||
); | ||
}, | ||
}, | ||
], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters