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", "base64-stream": "^1.0.0",
"clean-html": "^1.5.0", "clean-html": "^1.5.0",
"compare-version": "^0.1.2", "compare-version": "^0.1.2",
"countable": "^3.0.1",
"diacritics": "^1.3.0", "diacritics": "^1.3.0",
"diff-match-patch": "^1.0.4", "diff-match-patch": "^1.0.4",
"es6-promise-pool": "^2.5.0", "es6-promise-pool": "^2.5.0",
@ -90,6 +91,7 @@
"read-chunk": "^2.1.0", "read-chunk": "^2.1.0",
"redux": "^3.7.2", "redux": "^3.7.2",
"relative": "^3.0.2", "relative": "^3.0.2",
"remove-markdown": "^0.3.0",
"request": "^2.88.0", "request": "^2.88.0",
"sax": "^1.2.4", "sax": "^1.2.4",
"server-destroy": "^1.0.1", "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 { _ } = require('lib/locale.js');
const { themeStyle } = require('../theme.js'); const { themeStyle } = require('../theme.js');
const DialogButtonRow = require('./DialogButtonRow.min'); const DialogButtonRow = require('./DialogButtonRow.min');
const Countable = require('countable'); const { stripMarkdown } = require('lib/markdownUtils');
const { countElements } = require('lib/string-utils');
interface NoteContentPropertiesDialogProps { interface NoteContentPropertiesDialogProps {
theme: number, theme: number,
@ -21,19 +22,25 @@ interface KeyToLabelMap {
export default function NoteContentPropertiesDialog(props:NoteContentPropertiesDialogProps) { export default function NoteContentPropertiesDialog(props:NoteContentPropertiesDialogProps) {
const theme = themeStyle(props.theme); const theme = themeStyle(props.theme);
const textComps: JSX.Element[] = []; const tableBodyComps: JSX.Element[] = [];
// For the source Markdown
const [lines, setLines] = useState<number>(0); const [lines, setLines] = useState<number>(0);
const [words, setWords] = useState<number>(0); const [words, setWords] = useState<number>(0);
const [characters, setCharacters] = useState<number>(0); const [characters, setCharacters] = useState<number>(0);
const [charactersNoSpace, setCharactersNoSpace] = 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(() => { useEffect(() => {
Countable.count(props.text, (counter: { words: number; all: number; characters: number; }) => { countElements(props.text, setWords, setCharacters, setCharactersNoSpace, setLines);
setWords(counter.words); }, [props.text]);
setCharacters(counter.all);
setCharactersNoSpace(counter.characters); useEffect(() => {
}); const strippedText: string = stripMarkdown(props.text);
props.text === '' ? setLines(0) : setLines(props.text.split('\n').length); countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedLines);
}, [props.text]); }, [props.text]);
const textProperties: TextPropertiesMap = { const textProperties: TextPropertiesMap = {
@ -43,6 +50,13 @@ export default function NoteContentPropertiesDialog(props:NoteContentPropertiesD
charactersNoSpace: charactersNoSpace, charactersNoSpace: charactersNoSpace,
}; };
const strippedTextProperties: TextPropertiesMap = {
lines: strippedLines,
words: strippedWords,
characters: strippedCharacters,
charactersNoSpace: strippedCharactersNoSpace,
};
const keyToLabel: KeyToLabelMap = { const keyToLabel: KeyToLabelMap = {
words: _('Words'), words: _('Words'),
characters: _('Characters'), characters: _('Characters'),
@ -54,28 +68,62 @@ export default function NoteContentPropertiesDialog(props:NoteContentPropertiesD
props.onClose(); props.onClose();
}; };
const createItemField = (key: string, value: number) => { const labelCompStyle = {
const labelComp = <label style={Object.assign({}, theme.textStyle, theme.controlBoxLabel)}>{keyToLabel[key]}</label>; ...theme.textStyle,
const controlComp = <div style={Object.assign({}, theme.textStyle, theme.controlBoxValue)}>{value}</div>; 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 ( return (
<div key={key} style={theme.controlBox} className="note-text-property-box">{labelComp}{controlComp}</div> <tr key={key}>{labelComp}{controlComp}{strippedControlComp}</tr>
); );
}; };
if (textProperties) { 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) { for (const key in textProperties) {
if (!textProperties.hasOwnProperty(key)) continue; const comp = createTableBodyRow(key, textProperties[key], strippedTextProperties[key]);
const comp = createItemField(key, textProperties[key]); tableBodyComps.push(comp);
textComps.push(comp);
}
} }
const dialogBoxHeadingStyle = {
...theme.dialogTitle,
textAlign: 'center',
};
return ( return (
<div style={theme.dialogModalLayer}> <div style={theme.dialogModalLayer}>
<div style={theme.dialogBox}> <div style={theme.dialogBox}>
<div style={theme.dialogTitle}>{_('Statistics')}</div> <div style={dialogBoxHeadingStyle}>{_('Content properties')}</div>
<div>{textComps}</div> <table>
<thead>
{tableHeader}
</thead>
<tbody>
{tableBodyComps}
</tbody>
</table>
<DialogButtonRow theme={props.theme} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/> <DialogButtonRow theme={props.theme} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
</div> </div>
</div> </div>

View File

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

View File

@ -2,6 +2,7 @@ const stringPadding = require('string-padding');
const urlUtils = require('lib/urlUtils'); const urlUtils = require('lib/urlUtils');
const MarkdownIt = require('markdown-it'); const MarkdownIt = require('markdown-it');
const { setupLinkify } = require('lib/joplin-renderer'); const { setupLinkify } = require('lib/joplin-renderer');
const removeMarkdown = require('remove-markdown');
const markdownUtils = { const markdownUtils = {
// Not really escaping because that's not supported by marked.js // Not really escaping because that's not supported by marked.js
@ -101,6 +102,11 @@ const markdownUtils = {
const title = lines[0].trim(); const title = lines[0].trim();
return title.replace(filterRegex, '').replace(mdLinkRegex, '$1').replace(emptyMdLinkRegex, '$1').substring(0,80); 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; module.exports = markdownUtils;

View File

@ -1,4 +1,5 @@
const stringUtilsCommon = require('./string-utils-common.js'); const stringUtilsCommon = require('./string-utils-common.js');
const Countable = require('countable');
const defaultDiacriticsRemovalMap = [ 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 }, { 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'; 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);