文件
mynode/packages/app-desktop/gui/NoteContentPropertiesDialog.tsx
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

194 行
6.4 KiB
TypeScript

import * as React from 'react';
import { useState, useEffect } from 'react';
import { _ } from '@joplin/lib/locale';
import DialogButtonRow from './DialogButtonRow';
import { themeStyle } from '@joplin/lib/theme';
const Countable = require('@joplin/lib/countable/Countable');
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils';
import Dialog from '@joplin/lib/components/Dialog';
interface NoteContentPropertiesDialogProps {
themeId: number;
text: string;
markupLanguage: number;
onClose: ()=> void;
}
interface TextPropertiesMap {
[key: string]: number;
}
interface KeyToLabelMap {
[key: string]: string;
}
let markupToHtml_: ReturnType<typeof markupLanguageUtils.newMarkupToHtml> = null;
function markupToHtml() {
if (markupToHtml_) return markupToHtml_;
markupToHtml_ = markupLanguageUtils.newMarkupToHtml();
return markupToHtml_;
}
interface CounterResult {
words: number;
all: number;
characters: number;
}
function countElements(text: string, wordSetter: React.Dispatch<React.SetStateAction<number>>, characterSetter: React.Dispatch<React.SetStateAction<number>>, characterNoSpaceSetter: React.Dispatch<React.SetStateAction<number>>, cjkCharacterSetter: React.Dispatch<React.SetStateAction<number>>, lineSetter: React.Dispatch<React.SetStateAction<number>>) {
Countable.count(text, (counter: CounterResult) => {
wordSetter(counter.words);
characterSetter(counter.all);
characterNoSpaceSetter(counter.characters);
});
const cjkMatches = text.match(/[\p{Script=Han}\p{Script=Bopomofo}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/gu);
cjkCharacterSetter(cjkMatches ? cjkMatches.length : 0);
lineSetter(text === '' ? 0 : text.split('\n').length);
}
function formatReadTime(readTimeMinutes: number) {
if (readTimeMinutes < 1) {
return '< 1';
}
return Math.ceil(readTimeMinutes).toString();
}
export default function NoteContentPropertiesDialog(props: NoteContentPropertiesDialogProps) {
const theme = themeStyle(props.themeId);
const tableBodyComps: React.JSX.Element[] = [];
// For the source Markdown
const [lines, setLines] = useState<number>(0);
const [words, setWords] = useState<number>(0);
const [characters, setCharacters] = useState<number>(0);
const [charactersNoSpace, setCharactersNoSpace] = useState<number>(0);
const [cjkCharacters, setCjkCharacters] = useState<number>(0);
// For source with Markdown syntax stripped out
const [strippedLines, setStrippedLines] = useState<number>(0);
const [strippedWords, setStrippedWords] = useState<number>(0);
const [strippedCharacters, setStrippedCharacters] = useState<number>(0);
const [strippedCharactersNoSpace, setStrippedCharactersNoSpace] = useState<number>(0);
const [strippedCjkCharacters, setStrippedCjkCharacters] = useState<number>(0);
const [strippedReadTime, setStrippedReadTime] = useState<number>(0);
// This amount based on the following paper:
// https://www.researchgate.net/publication/332380784_How_many_words_do_we_read_per_minute_A_review_and_meta-analysis_of_reading_rate
const wordsPerMinute = 250;
useEffect(() => {
countElements(props.text, setWords, setCharacters, setCharactersNoSpace, setCjkCharacters, setLines);
}, [props.text]);
useEffect(() => {
const strippedText: string = markupToHtml().stripMarkup(props.markupLanguage, props.text);
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedCjkCharacters, setStrippedLines);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.text]);
useEffect(() => {
const readTimeMinutes: number = strippedWords / wordsPerMinute;
setStrippedReadTime(readTimeMinutes);
}, [strippedWords]);
const textProperties: TextPropertiesMap = {
lines: lines,
words: words,
characters: characters,
charactersNoSpace: charactersNoSpace,
cjkCharacters: cjkCharacters,
};
const strippedTextProperties: TextPropertiesMap = {
// The function stripMarkup() currently removes all new lines so we can't use the
// strippedLines property. Instead we simply use the lines property which should
// be a good approximation anyway.
// Also dummy check to silence TypeScript warning
lines: strippedLines === -5000 ? strippedLines : lines,
words: strippedWords,
characters: strippedCharacters,
charactersNoSpace: strippedCharactersNoSpace,
cjkCharacters: strippedCjkCharacters,
};
const keyToLabel: KeyToLabelMap = {
words: _('Words'),
characters: _('Characters'),
charactersNoSpace: _('Characters excluding spaces'),
cjkCharacters: _('Chinese/Japanese/Korean characters'),
lines: _('Lines'),
};
const buttonRow_click = () => {
props.onClose();
};
const labelCompStyle = {
...theme.textStyle,
fontWeight: 'bold',
width: '10em',
};
const controlCompStyle: React.CSSProperties = {
...theme.textStyle,
textAlign: 'center',
};
const createTableBodyRow = (key: string, value: number, strippedValue: number) => {
const labelComp = <td style={labelCompStyle}>{keyToLabel[key]}</td>;
const controlComp = <td style={controlCompStyle}>{value}</td>;
const strippedControlComp = <td style={controlCompStyle}>{strippedValue}</td>;
return (
<tr key={key}>{labelComp}{controlComp}{strippedControlComp}</tr>
);
};
const tableHeaderStyle: React.CSSProperties = {
...theme.textStyle,
textAlign: 'center',
};
const tableHeader = (
<tr>
<th style={tableHeaderStyle}></th>
<th style={tableHeaderStyle}>{_('Editor')}</th>
<th style={tableHeaderStyle}>{_('Viewer')}</th>
</tr>
);
for (const key in textProperties) {
if (key === 'cjkCharacters' && textProperties[key] === 0 && strippedTextProperties[key] === 0) continue;
const comp = createTableBodyRow(key, textProperties[key], strippedTextProperties[key]);
tableBodyComps.push(comp);
}
const dialogBoxHeadingStyle: React.CSSProperties = {
...theme.dialogTitle,
textAlign: 'left',
};
const readTimeLabel = _('Read time: %s min', formatReadTime(strippedReadTime));
return (
<Dialog onCancel={props.onClose}>
<div style={dialogBoxHeadingStyle}>{_('Statistics')}</div>
<table>
<thead>
{tableHeader}
</thead>
<tbody>
{tableBodyComps}
</tbody>
</table>
<div style={{ ...labelCompStyle, marginTop: 10 }}>
{readTimeLabel}
</div>
<DialogButtonRow
themeId={props.themeId}
onClick={buttonRow_click}
okButtonShow={false}
cancelButtonLabel={_('Close')}
/>
</Dialog>
);
}