mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-23 19:47:03 -04:00
Add word count in document info modal (#738)
Co-authored-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
4b3990d0db
commit
57f46f489b
15 changed files with 242 additions and 9 deletions
|
@ -27,6 +27,7 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
|||
private onRendererReadyHandler?: () => void
|
||||
private onImageClickedHandler?: (details: ImageDetails) => void
|
||||
private onHeightChangeHandler?: (height: number) => void
|
||||
private onWordCountCalculatedHandler?: (words: number) => void
|
||||
|
||||
public onHeightChange(handler?: (height: number) => void): void {
|
||||
this.onHeightChangeHandler = handler
|
||||
|
@ -60,6 +61,10 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
|||
this.onSetScrollStateHandler = handler
|
||||
}
|
||||
|
||||
public onWordCountCalculated(handler?: (words: number) => void): void {
|
||||
this.onWordCountCalculatedHandler = handler
|
||||
}
|
||||
|
||||
public sendSetBaseConfiguration(baseConfiguration: BaseConfiguration): void {
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.SET_BASE_CONFIGURATION,
|
||||
|
@ -91,6 +96,12 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
|||
})
|
||||
}
|
||||
|
||||
public sendGetWordCount(): void {
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.GET_WORD_COUNT
|
||||
})
|
||||
}
|
||||
|
||||
protected handleEvent(event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
|
||||
const renderMessage = event.data
|
||||
switch (renderMessage.type) {
|
||||
|
@ -118,6 +129,9 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
|||
case RenderIframeMessageType.ON_HEIGHT_CHANGE:
|
||||
this.onHeightChangeHandler?.(renderMessage.height)
|
||||
return false
|
||||
case RenderIframeMessageType.ON_WORD_COUNT_CALCULATED:
|
||||
this.onWordCountCalculatedHandler?.(renderMessage.words)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { ImageClickHandler } from '../markdown-renderer/replace-components/image
|
|||
import { useImageClickHandler } from './hooks/use-image-click-handler'
|
||||
import { MarkdownDocument } from './markdown-document'
|
||||
import { useIFrameRendererToEditorCommunicator } from '../editor-page/render-context/iframe-renderer-to-editor-communicator-context-provider'
|
||||
import { countWords } from './word-counter'
|
||||
|
||||
export const IframeMarkdownRenderer: React.FC = () => {
|
||||
const [markdownContent, setMarkdownContent] = useState('')
|
||||
|
@ -22,10 +23,24 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
|||
|
||||
const iframeCommunicator = useIFrameRendererToEditorCommunicator()
|
||||
|
||||
const countWordsInRenderedDocument = useCallback(() => {
|
||||
const documentContainer = document.querySelector('.markdown-body')
|
||||
if (!documentContainer) {
|
||||
iframeCommunicator?.sendWordCountCalculated(0)
|
||||
return
|
||||
}
|
||||
const wordCount = countWords(documentContainer)
|
||||
iframeCommunicator?.sendWordCountCalculated(wordCount)
|
||||
}, [iframeCommunicator])
|
||||
|
||||
useEffect(() => iframeCommunicator?.onSetBaseConfiguration(setBaseConfiguration), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator?.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator?.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator?.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
|
||||
useEffect(
|
||||
() => iframeCommunicator?.onGetWordCount(countWordsInRenderedDocument),
|
||||
[iframeCommunicator, countWordsInRenderedDocument]
|
||||
)
|
||||
|
||||
const onTaskCheckedChange = useCallback(
|
||||
(lineInMarkdown: number, checked: boolean) => {
|
||||
|
|
|
@ -23,6 +23,7 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
|||
private onSetDarkModeHandler?: (darkModeActivated: boolean) => void
|
||||
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
||||
private onSetBaseConfigurationHandler?: (baseConfiguration: BaseConfiguration) => void
|
||||
private onGetWordCountHandler?: () => void
|
||||
|
||||
public onSetBaseConfiguration(handler?: (baseConfiguration: BaseConfiguration) => void): void {
|
||||
this.onSetBaseConfigurationHandler = handler
|
||||
|
@ -40,6 +41,10 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
|||
this.onSetScrollStateHandler = handler
|
||||
}
|
||||
|
||||
public onGetWordCount(handler?: () => void): void {
|
||||
this.onGetWordCountHandler = handler
|
||||
}
|
||||
|
||||
public sendRendererReady(): void {
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.RENDERER_READY
|
||||
|
@ -95,6 +100,13 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
|||
})
|
||||
}
|
||||
|
||||
public sendWordCountCalculated(words: number): void {
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.ON_WORD_COUNT_CALCULATED,
|
||||
words
|
||||
})
|
||||
}
|
||||
|
||||
protected handleEvent(event: MessageEvent<EditorToRendererIframeMessage>): boolean | undefined {
|
||||
const renderMessage = event.data
|
||||
switch (renderMessage.type) {
|
||||
|
@ -110,6 +122,9 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
|||
case RenderIframeMessageType.SET_BASE_CONFIGURATION:
|
||||
this.onSetBaseConfigurationHandler?.(renderMessage.baseConfiguration)
|
||||
return false
|
||||
case RenderIframeMessageType.GET_WORD_COUNT:
|
||||
this.onGetWordCountHandler?.()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,9 @@ export enum RenderIframeMessageType {
|
|||
ON_SET_FRONTMATTER = 'ON_SET_FRONTMATTER',
|
||||
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
||||
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
|
||||
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION'
|
||||
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION',
|
||||
GET_WORD_COUNT = 'GET_WORD_COUNT',
|
||||
ON_WORD_COUNT_CALCULATED = 'ON_WORD_COUNT_CALCULATED'
|
||||
}
|
||||
|
||||
export interface RendererToEditorSimpleMessage {
|
||||
|
@ -40,6 +42,10 @@ export interface SetBaseUrlMessage {
|
|||
baseConfiguration: BaseConfiguration
|
||||
}
|
||||
|
||||
export interface GetWordCountMessage {
|
||||
type: RenderIframeMessageType.GET_WORD_COUNT
|
||||
}
|
||||
|
||||
export interface ImageClickedMessage {
|
||||
type: RenderIframeMessageType.IMAGE_CLICKED
|
||||
details: ImageDetails
|
||||
|
@ -76,11 +82,17 @@ export interface OnHeightChangeMessage {
|
|||
height: number
|
||||
}
|
||||
|
||||
export interface OnWordCountCalculatedMessage {
|
||||
type: RenderIframeMessageType.ON_WORD_COUNT_CALCULATED
|
||||
words: number
|
||||
}
|
||||
|
||||
export type EditorToRendererIframeMessage =
|
||||
| SetMarkdownContentMessage
|
||||
| SetDarkModeMessage
|
||||
| SetScrollStateMessage
|
||||
| SetBaseUrlMessage
|
||||
| GetWordCountMessage
|
||||
|
||||
export type RendererToEditorIframeMessage =
|
||||
| RendererToEditorSimpleMessage
|
||||
|
@ -90,6 +102,7 @@ export type RendererToEditorIframeMessage =
|
|||
| SetScrollStateMessage
|
||||
| ImageClickedMessage
|
||||
| OnHeightChangeMessage
|
||||
| OnWordCountCalculatedMessage
|
||||
|
||||
export enum RendererType {
|
||||
DOCUMENT,
|
||||
|
|
50
src/components/render-page/word-counter.ts
Normal file
50
src/components/render-page/word-counter.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import wordsCount from 'words-count'
|
||||
|
||||
/** List of HTML tag names that should not be counted. */
|
||||
const EXCLUDED_TAGS = ['img', 'pre', 'nav']
|
||||
/** List of class names that should not be counted. */
|
||||
const EXCLUDED_CLASSES = ['katex-mathml']
|
||||
|
||||
/**
|
||||
* Checks whether the given node is an excluded HTML tag and therefore should be
|
||||
* excluded from counting.
|
||||
* @param node The node to test.
|
||||
* @return true if the node should be excluded, false otherwise.
|
||||
*/
|
||||
const isExcludedTag = (node: Element | ChildNode): boolean => {
|
||||
return EXCLUDED_TAGS.includes(node.nodeName.toLowerCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node is a HTML element with an excluded class name,
|
||||
* so that it should be excluded.
|
||||
* @param node The node to test.
|
||||
* @return true if the node should be excluded, false otherwise.
|
||||
*/
|
||||
const isExcludedClass = (node: Element | ChildNode): boolean => {
|
||||
return EXCLUDED_CLASSES.some((excludedClass) => (node as HTMLElement).classList?.contains(excludedClass))
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the words of the given node while ignoring empty nodes and excluded
|
||||
* nodes. Child nodes will recursively counted as well.
|
||||
* @param node The node whose content's words should be counted.
|
||||
* @return The number of words counted in this node and its children.
|
||||
*/
|
||||
export const countWords = (node: Element | ChildNode): number => {
|
||||
if (!node.textContent || isExcludedTag(node) || isExcludedClass(node)) {
|
||||
return 0
|
||||
}
|
||||
if (!node.hasChildNodes()) {
|
||||
return wordsCount(node.textContent)
|
||||
}
|
||||
return [...node.childNodes].reduce((words, childNode) => {
|
||||
return words + countWords(childNode)
|
||||
}, 0)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue