1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Desktop: Fixes #3009: Word/character counter includes Markdown syntax and HTML tags (#3037)

* Updated commit

* Update package.json

* Update package.json

Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
This commit is contained in:
RedDocMD 2020-05-15 15:58:31 +05:30 committed by GitHub
parent 7d767cf0c9
commit a09c7b72c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 125 additions and 21 deletions

View File

@ -41,6 +41,7 @@
"base64-stream": "^1.0.0",
"clean-html": "^1.5.0",
"compare-version": "^0.1.2",
"countable": "^3.0.1",
"diacritics": "^1.3.0",
"diff-match-patch": "^1.0.4",
"es6-promise-pool": "^2.5.0",
@ -90,6 +91,7 @@
"read-chunk": "^2.1.0",
"redux": "^3.7.2",
"relative": "^3.0.2",
"remove-markdown": "^0.3.0",
"request": "^2.88.0",
"sax": "^1.2.4",
"server-destroy": "^1.0.1",

View File

@ -85,4 +85,41 @@ describe('markdownUtils', function() {
}
}));
it('should remove Markdown syntax elements from the text', asyncTest(async () => {
const inputStrings = [
'', // Empty string
'This is some plain text', // Plain text
'## This is a header', // Header syntax
'This is a text with **bold** and *italicized* text', // Text with annotations
'This is a text with __bold__ and _italicized_ text', // Text with annotations alternate form
'[link to google](https://www.google.com/)', // Link
'> This is a blockquote\n And another line', // Blockquote
'* List item\n* List item', // Unordered list
'- List item\n- List item', // Unordered list
'1. List item\n2. List item', // Ordered list
'This is some `inline code`', // Inlined code
];
const expectedOutputStrings = [
'',
'This is some plain text',
'This is a header',
'This is a text with bold and italicized text',
'This is a text with bold and italicized text',
'link to google',
'This is a blockquote\n And another line',
'List item\nList item',
'List item\nList item',
'List item\nList item',
'This is some inline code',
];
expect(inputStrings.length).toBe(expectedOutputStrings.length);
for (let i = 0; i < inputStrings.length; i++) {
const outputString = markdownUtils.stripMarkdown(inputStrings[i]);
expect(outputString).toBe(expectedOutputStrings[i]);
}
}));
});

View File

@ -3,7 +3,8 @@ import { useState, useEffect } from 'react';
const { _ } = require('lib/locale.js');
const { themeStyle } = require('../theme.js');
const DialogButtonRow = require('./DialogButtonRow.min');
const Countable = require('countable');
const { stripMarkdown } = require('lib/markdownUtils');
const { countElements } = require('lib/string-utils');
interface NoteContentPropertiesDialogProps {
theme: number,
@ -21,19 +22,25 @@ interface KeyToLabelMap {
export default function NoteContentPropertiesDialog(props:NoteContentPropertiesDialogProps) {
const theme = themeStyle(props.theme);
const textComps: JSX.Element[] = [];
const tableBodyComps: 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);
// 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);
useEffect(() => {
Countable.count(props.text, (counter: { words: number; all: number; characters: number; }) => {
setWords(counter.words);
setCharacters(counter.all);
setCharactersNoSpace(counter.characters);
});
props.text === '' ? setLines(0) : setLines(props.text.split('\n').length);
countElements(props.text, setWords, setCharacters, setCharactersNoSpace, setLines);
}, [props.text]);
useEffect(() => {
const strippedText: string = stripMarkdown(props.text);
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedLines);
}, [props.text]);
const textProperties: TextPropertiesMap = {
@ -43,6 +50,13 @@ export default function NoteContentPropertiesDialog(props:NoteContentPropertiesD
charactersNoSpace: charactersNoSpace,
};
const strippedTextProperties: TextPropertiesMap = {
lines: strippedLines,
words: strippedWords,
characters: strippedCharacters,
charactersNoSpace: strippedCharactersNoSpace,
};
const keyToLabel: KeyToLabelMap = {
words: _('Words'),
characters: _('Characters'),
@ -54,28 +68,62 @@ export default function NoteContentPropertiesDialog(props:NoteContentPropertiesD
props.onClose();
};
const createItemField = (key: string, value: number) => {
const labelComp = <label style={Object.assign({}, theme.textStyle, theme.controlBoxLabel)}>{keyToLabel[key]}</label>;
const controlComp = <div style={Object.assign({}, theme.textStyle, theme.controlBoxValue)}>{value}</div>;
const labelCompStyle = {
...theme.textStyle,
fontWeight: 'bold',
width: '10em',
};
const controlCompStyle = {
...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 (
<div key={key} style={theme.controlBox} className="note-text-property-box">{labelComp}{controlComp}</div>
<tr key={key}>{labelComp}{controlComp}{strippedControlComp}</tr>
);
};
if (textProperties) {
for (const key in textProperties) {
if (!textProperties.hasOwnProperty(key)) continue;
const comp = createItemField(key, textProperties[key]);
textComps.push(comp);
}
const tableHeaderStyle = {
...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) {
const comp = createTableBodyRow(key, textProperties[key], strippedTextProperties[key]);
tableBodyComps.push(comp);
}
const dialogBoxHeadingStyle = {
...theme.dialogTitle,
textAlign: 'center',
};
return (
<div style={theme.dialogModalLayer}>
<div style={theme.dialogBox}>
<div style={theme.dialogTitle}>{_('Statistics')}</div>
<div>{textComps}</div>
<div style={dialogBoxHeadingStyle}>{_('Content properties')}</div>
<table>
<thead>
{tableHeader}
</thead>
<tbody>
{tableBodyComps}
</tbody>
</table>
<DialogButtonRow theme={props.theme} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
</div>
</div>

View File

@ -164,6 +164,7 @@
"readability-node": "^0.1.0",
"redux": "^3.7.2",
"relative": "^3.0.2",
"remove-markdown": "^0.3.0",
"reselect": "^4.0.0",
"sax": "^1.2.4",
"server-destroy": "^1.0.1",

View File

@ -2,6 +2,7 @@ const stringPadding = require('string-padding');
const urlUtils = require('lib/urlUtils');
const MarkdownIt = require('markdown-it');
const { setupLinkify } = require('lib/joplin-renderer');
const removeMarkdown = require('remove-markdown');
const markdownUtils = {
// Not really escaping because that's not supported by marked.js
@ -101,6 +102,11 @@ const markdownUtils = {
const title = lines[0].trim();
return title.replace(filterRegex, '').replace(mdLinkRegex, '$1').replace(emptyMdLinkRegex, '$1').substring(0,80);
},
stripMarkdown(text, options = { gfm: false }) {
// Removes Markdown syntax elements from the given text
return removeMarkdown(text, options);
},
};
module.exports = markdownUtils;

View File

@ -1,4 +1,5 @@
const stringUtilsCommon = require('./string-utils-common.js');
const Countable = require('countable');
const defaultDiacriticsRemovalMap = [
{ base: 'A', letters: /[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g },
@ -285,4 +286,13 @@ function scriptType(s) {
return 'en';
}
module.exports = Object.assign({ removeDiacritics, substrWithEllipsis, nextWhitespaceIndex, escapeFilename, wrap, splitCommandString, padLeft, toTitleCase, urlDecode, escapeHtml, surroundKeywords, scriptType, commandArgumentsToString }, stringUtilsCommon);
function countElements(text, wordSetter, characterSetter, characterNoSpaceSetter, lineSetter) {
Countable.count(text, counter => {
wordSetter(counter.words);
characterSetter(counter.all);
characterNoSpaceSetter(counter.characters);
});
text === '' ? lineSetter(0) : lineSetter(text.split('\n').length);
}
module.exports = Object.assign({ removeDiacritics, substrWithEllipsis, nextWhitespaceIndex, escapeFilename, wrap, splitCommandString, padLeft, toTitleCase, urlDecode, escapeHtml, surroundKeywords, scriptType, commandArgumentsToString, countElements }, stringUtilsCommon);