mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-29 07:14:30 -05: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
|
@ -73,6 +73,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
- When pasting tables (e.g. from LibreOffice Calc or MS Excel) they get reformatted to markdown tables.
|
- When pasting tables (e.g. from LibreOffice Calc or MS Excel) they get reformatted to markdown tables.
|
||||||
- The history page supports URL parameters that allow bookmarking of a specific search of tags filter.
|
- The history page supports URL parameters that allow bookmarking of a specific search of tags filter.
|
||||||
- Users can change the pinning state of a note directly from the editor.
|
- Users can change the pinning state of a note directly from the editor.
|
||||||
|
- Note information dialog containing word count, revision count, last editor and creation time.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
43
cypress/integration/word-count.spec.ts
Normal file
43
cypress/integration/word-count.spec.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('Test word count with', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visitTestEditor()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('empty note', () => {
|
||||||
|
cy.codemirrorFill('')
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get('[data-cy="sidebar-btn-document-info"]').click()
|
||||||
|
cy.get('[data-cy="document-info-modal"]').should('be.visible')
|
||||||
|
cy.get('[data-cy="document-info-word-count"]').should('have.text', '0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('simple words', () => {
|
||||||
|
cy.codemirrorFill('five words should be enough')
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get('[data-cy="sidebar-btn-document-info"]').click()
|
||||||
|
cy.get('[data-cy="document-info-modal"]').should('be.visible')
|
||||||
|
cy.get('[data-cy="document-info-word-count"]').should('have.text', '5')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('excluded codeblocks', () => {
|
||||||
|
cy.codemirrorFill('```\nthis is should be ignored\n```\n\ntwo `words`')
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get('[data-cy="sidebar-btn-document-info"]').click()
|
||||||
|
cy.get('[data-cy="document-info-modal"]').should('be.visible')
|
||||||
|
cy.get('[data-cy="document-info-word-count"]').should('have.text', '2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('excluded images', () => {
|
||||||
|
cy.codemirrorFill('![ignored alt text](https://dummyimage.com/48) not ignored text')
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get('[data-cy="sidebar-btn-document-info"]').click()
|
||||||
|
cy.get('[data-cy="document-info-modal"]').should('be.visible')
|
||||||
|
cy.get('[data-cy="document-info-word-count"]').should('have.text', '3')
|
||||||
|
})
|
||||||
|
})
|
|
@ -105,7 +105,8 @@
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"vega": "5.20.2",
|
"vega": "5.20.2",
|
||||||
"vega-embed": "6.17.0",
|
"vega-embed": "6.17.0",
|
||||||
"vega-lite": "5.0.0"
|
"vega-lite": "5.0.0",
|
||||||
|
"words-count": "1.0.8"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env PORT=3001 craco start",
|
"start": "cross-env PORT=3001 craco start",
|
||||||
|
|
|
@ -347,7 +347,8 @@
|
||||||
"created": "<0></0> created this note <1></1>",
|
"created": "<0></0> created this note <1></1>",
|
||||||
"edited": "<0></0> was the last editor <1></1>",
|
"edited": "<0></0> was the last editor <1></1>",
|
||||||
"usersContributed": "<0></0> users contributed to this document",
|
"usersContributed": "<0></0> users contributed to this document",
|
||||||
"revisions": "<0></0> revisions are saved"
|
"revisions": "<0></0> revisions are saved",
|
||||||
|
"words": "<0></0> words in document"
|
||||||
},
|
},
|
||||||
"gistImport": {
|
"gistImport": {
|
||||||
"title": "Import from Gist",
|
"title": "Import from Gist",
|
||||||
|
@ -458,6 +459,7 @@
|
||||||
"and": "and",
|
"and": "and",
|
||||||
"avatarOf": "avatar of '{{name}}'",
|
"avatarOf": "avatar of '{{name}}'",
|
||||||
"why": "Why?",
|
"why": "Why?",
|
||||||
|
"loading": "Loading ...",
|
||||||
"successfullyCopied": "Copied!",
|
"successfullyCopied": "Copied!",
|
||||||
"copyError": "Error while copying!",
|
"copyError": "Error while copying!",
|
||||||
"errorOccurred": "An error occurred",
|
"errorOccurred": "An error occurred",
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { ShowIf } from '../../../common/show-if/show-if'
|
||||||
|
import { DocumentInfoLine } from './document-info-line'
|
||||||
|
import { UnitalicBoldText } from './unitalic-bold-text'
|
||||||
|
import { useIFrameEditorToRendererCommunicator } from '../../render-context/iframe-editor-to-renderer-communicator-context-provider'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new info line for the document information dialog that holds the
|
||||||
|
* word count of the note, based on counting in the rendered output.
|
||||||
|
*/
|
||||||
|
export const DocumentInfoLineWordCount: React.FC = () => {
|
||||||
|
useTranslation()
|
||||||
|
const iframeEditorToRendererCommunicator = useIFrameEditorToRendererCommunicator()
|
||||||
|
const [wordCount, setWordCount] = useState<number | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
iframeEditorToRendererCommunicator?.onWordCountCalculated((words) => {
|
||||||
|
setWordCount(words)
|
||||||
|
})
|
||||||
|
iframeEditorToRendererCommunicator?.sendGetWordCount()
|
||||||
|
return () => {
|
||||||
|
iframeEditorToRendererCommunicator?.onWordCountCalculated(undefined)
|
||||||
|
}
|
||||||
|
}, [iframeEditorToRendererCommunicator, setWordCount])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentInfoLine icon={'align-left'} size={'2x'}>
|
||||||
|
<ShowIf condition={wordCount === null}>
|
||||||
|
<Trans i18nKey={'common.loading'} />
|
||||||
|
</ShowIf>
|
||||||
|
<ShowIf condition={wordCount !== null}>
|
||||||
|
<Trans i18nKey={'editor.modal.documentInfo.words'}>
|
||||||
|
<UnitalicBoldText text={wordCount ?? ''} dataCy={'document-info-word-count'} />
|
||||||
|
</Trans>
|
||||||
|
</ShowIf>
|
||||||
|
</DocumentInfoLine>
|
||||||
|
)
|
||||||
|
}
|
|
@ -7,12 +7,13 @@
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { ListGroup, Modal } from 'react-bootstrap'
|
import { ListGroup, Modal } from 'react-bootstrap'
|
||||||
import { Trans } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { CommonModal } from '../../../common/modals/common-modal'
|
import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
import { DocumentInfoLine } from './document-info-line'
|
import { DocumentInfoLine } from './document-info-line'
|
||||||
import { DocumentInfoLineWithTimeMode, DocumentInfoTimeLine } from './document-info-time-line'
|
import { DocumentInfoLineWithTimeMode, DocumentInfoTimeLine } from './document-info-time-line'
|
||||||
import { UnitalicBoldText } from './unitalic-bold-text'
|
import { UnitalicBoldText } from './unitalic-bold-text'
|
||||||
import { useCustomizeAssetsUrl } from '../../../../hooks/common/use-customize-assets-url'
|
import { useCustomizeAssetsUrl } from '../../../../hooks/common/use-customize-assets-url'
|
||||||
|
import { DocumentInfoLineWordCount } from './document-info-line-word-count'
|
||||||
|
|
||||||
export interface DocumentInfoModalProps {
|
export interface DocumentInfoModalProps {
|
||||||
show: boolean
|
show: boolean
|
||||||
|
@ -21,10 +22,16 @@ export interface DocumentInfoModalProps {
|
||||||
|
|
||||||
export const DocumentInfoModal: React.FC<DocumentInfoModalProps> = ({ show, onHide }) => {
|
export const DocumentInfoModal: React.FC<DocumentInfoModalProps> = ({ show, onHide }) => {
|
||||||
const assetsBaseUrl = useCustomizeAssetsUrl()
|
const assetsBaseUrl = useCustomizeAssetsUrl()
|
||||||
|
useTranslation()
|
||||||
|
|
||||||
// TODO Replace hardcoded mock data with real/mock API requests
|
// TODO Replace hardcoded mock data with real/mock API requests
|
||||||
return (
|
return (
|
||||||
<CommonModal show={show} onHide={onHide} closeButton={true} titleI18nKey={'editor.modal.documentInfo.title'}>
|
<CommonModal
|
||||||
|
show={show}
|
||||||
|
onHide={onHide}
|
||||||
|
closeButton={true}
|
||||||
|
titleI18nKey={'editor.modal.documentInfo.title'}
|
||||||
|
data-cy={'document-info-modal'}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
|
@ -59,6 +66,9 @@ export const DocumentInfoModal: React.FC<DocumentInfoModalProps> = ({ show, onHi
|
||||||
</Trans>
|
</Trans>
|
||||||
</DocumentInfoLine>
|
</DocumentInfoLine>
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
|
<ListGroup.Item>
|
||||||
|
<DocumentInfoLineWordCount />
|
||||||
|
</ListGroup.Item>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</CommonModal>
|
</CommonModal>
|
||||||
|
|
|
@ -7,9 +7,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export interface UnitalicBoldTextProps {
|
export interface UnitalicBoldTextProps {
|
||||||
text: string
|
text: string | number
|
||||||
|
dataCy?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UnitalicBoldText: React.FC<UnitalicBoldTextProps> = ({ text }) => {
|
export const UnitalicBoldText: React.FC<UnitalicBoldTextProps> = ({ text, dataCy }) => {
|
||||||
return <b className={'font-style-normal mr-1'}>{text}</b>
|
return (
|
||||||
|
<b className={'font-style-normal mr-1'} data-cy={dataCy}>
|
||||||
|
{text}
|
||||||
|
</b>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,12 @@ export const DocumentInfoSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<SidebarButton hide={hide} className={className} icon={'line-chart'} onClick={() => setShowModal(true)}>
|
<SidebarButton
|
||||||
|
hide={hide}
|
||||||
|
className={className}
|
||||||
|
icon={'line-chart'}
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
data-cy={'sidebar-btn-document-info'}>
|
||||||
<Trans i18nKey={'editor.modal.documentInfo.title'} />
|
<Trans i18nKey={'editor.modal.documentInfo.title'} />
|
||||||
</SidebarButton>
|
</SidebarButton>
|
||||||
<DocumentInfoModal show={showModal} onHide={() => setShowModal(false)} />
|
<DocumentInfoModal show={showModal} onHide={() => setShowModal(false)} />
|
||||||
|
|
|
@ -27,6 +27,7 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
||||||
private onRendererReadyHandler?: () => void
|
private onRendererReadyHandler?: () => void
|
||||||
private onImageClickedHandler?: (details: ImageDetails) => void
|
private onImageClickedHandler?: (details: ImageDetails) => void
|
||||||
private onHeightChangeHandler?: (height: number) => void
|
private onHeightChangeHandler?: (height: number) => void
|
||||||
|
private onWordCountCalculatedHandler?: (words: number) => void
|
||||||
|
|
||||||
public onHeightChange(handler?: (height: number) => void): void {
|
public onHeightChange(handler?: (height: number) => void): void {
|
||||||
this.onHeightChangeHandler = handler
|
this.onHeightChangeHandler = handler
|
||||||
|
@ -60,6 +61,10 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
||||||
this.onSetScrollStateHandler = handler
|
this.onSetScrollStateHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onWordCountCalculated(handler?: (words: number) => void): void {
|
||||||
|
this.onWordCountCalculatedHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
public sendSetBaseConfiguration(baseConfiguration: BaseConfiguration): void {
|
public sendSetBaseConfiguration(baseConfiguration: BaseConfiguration): void {
|
||||||
this.sendMessageToOtherSide({
|
this.sendMessageToOtherSide({
|
||||||
type: RenderIframeMessageType.SET_BASE_CONFIGURATION,
|
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 {
|
protected handleEvent(event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
|
||||||
const renderMessage = event.data
|
const renderMessage = event.data
|
||||||
switch (renderMessage.type) {
|
switch (renderMessage.type) {
|
||||||
|
@ -118,6 +129,9 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
||||||
case RenderIframeMessageType.ON_HEIGHT_CHANGE:
|
case RenderIframeMessageType.ON_HEIGHT_CHANGE:
|
||||||
this.onHeightChangeHandler?.(renderMessage.height)
|
this.onHeightChangeHandler?.(renderMessage.height)
|
||||||
return false
|
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 { useImageClickHandler } from './hooks/use-image-click-handler'
|
||||||
import { MarkdownDocument } from './markdown-document'
|
import { MarkdownDocument } from './markdown-document'
|
||||||
import { useIFrameRendererToEditorCommunicator } from '../editor-page/render-context/iframe-renderer-to-editor-communicator-context-provider'
|
import { useIFrameRendererToEditorCommunicator } from '../editor-page/render-context/iframe-renderer-to-editor-communicator-context-provider'
|
||||||
|
import { countWords } from './word-counter'
|
||||||
|
|
||||||
export const IframeMarkdownRenderer: React.FC = () => {
|
export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
const [markdownContent, setMarkdownContent] = useState('')
|
const [markdownContent, setMarkdownContent] = useState('')
|
||||||
|
@ -22,10 +23,24 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
|
|
||||||
const iframeCommunicator = useIFrameRendererToEditorCommunicator()
|
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?.onSetBaseConfiguration(setBaseConfiguration), [iframeCommunicator])
|
||||||
useEffect(() => iframeCommunicator?.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
|
useEffect(() => iframeCommunicator?.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
|
||||||
useEffect(() => iframeCommunicator?.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
useEffect(() => iframeCommunicator?.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
||||||
useEffect(() => iframeCommunicator?.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
|
useEffect(() => iframeCommunicator?.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
|
||||||
|
useEffect(
|
||||||
|
() => iframeCommunicator?.onGetWordCount(countWordsInRenderedDocument),
|
||||||
|
[iframeCommunicator, countWordsInRenderedDocument]
|
||||||
|
)
|
||||||
|
|
||||||
const onTaskCheckedChange = useCallback(
|
const onTaskCheckedChange = useCallback(
|
||||||
(lineInMarkdown: number, checked: boolean) => {
|
(lineInMarkdown: number, checked: boolean) => {
|
||||||
|
|
|
@ -23,6 +23,7 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
||||||
private onSetDarkModeHandler?: (darkModeActivated: boolean) => void
|
private onSetDarkModeHandler?: (darkModeActivated: boolean) => void
|
||||||
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
||||||
private onSetBaseConfigurationHandler?: (baseConfiguration: BaseConfiguration) => void
|
private onSetBaseConfigurationHandler?: (baseConfiguration: BaseConfiguration) => void
|
||||||
|
private onGetWordCountHandler?: () => void
|
||||||
|
|
||||||
public onSetBaseConfiguration(handler?: (baseConfiguration: BaseConfiguration) => void): void {
|
public onSetBaseConfiguration(handler?: (baseConfiguration: BaseConfiguration) => void): void {
|
||||||
this.onSetBaseConfigurationHandler = handler
|
this.onSetBaseConfigurationHandler = handler
|
||||||
|
@ -40,6 +41,10 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
||||||
this.onSetScrollStateHandler = handler
|
this.onSetScrollStateHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onGetWordCount(handler?: () => void): void {
|
||||||
|
this.onGetWordCountHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
public sendRendererReady(): void {
|
public sendRendererReady(): void {
|
||||||
this.sendMessageToOtherSide({
|
this.sendMessageToOtherSide({
|
||||||
type: RenderIframeMessageType.RENDERER_READY
|
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 {
|
protected handleEvent(event: MessageEvent<EditorToRendererIframeMessage>): boolean | undefined {
|
||||||
const renderMessage = event.data
|
const renderMessage = event.data
|
||||||
switch (renderMessage.type) {
|
switch (renderMessage.type) {
|
||||||
|
@ -110,6 +122,9 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
||||||
case RenderIframeMessageType.SET_BASE_CONFIGURATION:
|
case RenderIframeMessageType.SET_BASE_CONFIGURATION:
|
||||||
this.onSetBaseConfigurationHandler?.(renderMessage.baseConfiguration)
|
this.onSetBaseConfigurationHandler?.(renderMessage.baseConfiguration)
|
||||||
return false
|
return false
|
||||||
|
case RenderIframeMessageType.GET_WORD_COUNT:
|
||||||
|
this.onGetWordCountHandler?.()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,9 @@ export enum RenderIframeMessageType {
|
||||||
ON_SET_FRONTMATTER = 'ON_SET_FRONTMATTER',
|
ON_SET_FRONTMATTER = 'ON_SET_FRONTMATTER',
|
||||||
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
||||||
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
|
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 {
|
export interface RendererToEditorSimpleMessage {
|
||||||
|
@ -40,6 +42,10 @@ export interface SetBaseUrlMessage {
|
||||||
baseConfiguration: BaseConfiguration
|
baseConfiguration: BaseConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetWordCountMessage {
|
||||||
|
type: RenderIframeMessageType.GET_WORD_COUNT
|
||||||
|
}
|
||||||
|
|
||||||
export interface ImageClickedMessage {
|
export interface ImageClickedMessage {
|
||||||
type: RenderIframeMessageType.IMAGE_CLICKED
|
type: RenderIframeMessageType.IMAGE_CLICKED
|
||||||
details: ImageDetails
|
details: ImageDetails
|
||||||
|
@ -76,11 +82,17 @@ export interface OnHeightChangeMessage {
|
||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OnWordCountCalculatedMessage {
|
||||||
|
type: RenderIframeMessageType.ON_WORD_COUNT_CALCULATED
|
||||||
|
words: number
|
||||||
|
}
|
||||||
|
|
||||||
export type EditorToRendererIframeMessage =
|
export type EditorToRendererIframeMessage =
|
||||||
| SetMarkdownContentMessage
|
| SetMarkdownContentMessage
|
||||||
| SetDarkModeMessage
|
| SetDarkModeMessage
|
||||||
| SetScrollStateMessage
|
| SetScrollStateMessage
|
||||||
| SetBaseUrlMessage
|
| SetBaseUrlMessage
|
||||||
|
| GetWordCountMessage
|
||||||
|
|
||||||
export type RendererToEditorIframeMessage =
|
export type RendererToEditorIframeMessage =
|
||||||
| RendererToEditorSimpleMessage
|
| RendererToEditorSimpleMessage
|
||||||
|
@ -90,6 +102,7 @@ export type RendererToEditorIframeMessage =
|
||||||
| SetScrollStateMessage
|
| SetScrollStateMessage
|
||||||
| ImageClickedMessage
|
| ImageClickedMessage
|
||||||
| OnHeightChangeMessage
|
| OnHeightChangeMessage
|
||||||
|
| OnWordCountCalculatedMessage
|
||||||
|
|
||||||
export enum RendererType {
|
export enum RendererType {
|
||||||
DOCUMENT,
|
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)
|
||||||
|
}
|
9
src/external-types/words-count/words-count.d.ts
vendored
Normal file
9
src/external-types/words-count/words-count.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module 'words-count' {
|
||||||
|
export default function wordsCount(text: string): number
|
||||||
|
}
|
|
@ -15127,6 +15127,11 @@ word-wrap@^1.2.3, word-wrap@~1.2.3:
|
||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||||
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
||||||
|
|
||||||
|
words-count@1.0.8:
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/words-count/-/words-count-1.0.8.tgz#c048de08d49dd68e42d2aa47bcc43993a479b4f3"
|
||||||
|
integrity sha512-t/uFU5+JgvAm2MCeA9kqWZdNqkNLuzn1+k0gkY3HJ/l/2unEUU8Wrs8opB+BzTERKdH2QMcRU5UI4TJSnhMngA==
|
||||||
|
|
||||||
workbox-background-sync@^5.1.4:
|
workbox-background-sync@^5.1.4:
|
||||||
version "5.1.4"
|
version "5.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-5.1.4.tgz#5ae0bbd455f4e9c319e8d827c055bb86c894fd12"
|
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-5.1.4.tgz#5ae0bbd455f4e9c319e8d827c055bb86c894fd12"
|
||||||
|
|
Loading…
Reference in a new issue