diff --git a/README.md b/README.md index e718b93..7de7a1c 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ It does not require any additional licenses, except for MIT. ([#38](https://gith | [Custom `sep` Style](#options-sep) | ✅ | | [Fixation-Points](#options-fixationpoint) | ✅ | | [Ignore HTML Tags](#options-ignorehtmltag) | ✅ | +| [Ignore HTML Entity](#options-ignorehtmlentity) | ✅ | | [Saccade](https://github.com/Gumball12/text-vide/issues/21) | ❌ | ### Benchmark @@ -123,7 +124,7 @@ type Options = Partial<{ #### `sep` -- Default Value: `['', '']` +- Default: `['', '']` Passing a string allows you to specify the Beginning and End of the highlighted word at once. @@ -139,7 +140,7 @@ textVide('text-vide', { sep: ['', ''] }); // 'tex -- Default Value: `1` +- Default: `1` - Range: `[1, 5]` ```ts @@ -152,7 +153,7 @@ textVide('text-vide', { fixationPoint: 5 }); // 'text-vide' #### `ignoreHtmlTag` -- Default Value: `true` +- Default: `true` If this option is `true`, HTML tags are not highlighted. @@ -161,6 +162,17 @@ textVite('
abcd
efg'); // '
abcd
efg' textVite('
abcd
efg', { ignoreHtmlTag: false }); // '<div>abcddiv>efg' ``` +#### `ignoreHtmlEntity` + +- Default: `true` + +If this option is `true`, HTML entities are not highlighted. + +```ts +textVide(' abcd>'); // ' abcd>' +textVide(' abcd>', { ignoreHtmlEntity: false }); // &nbsp;abcd&gt; +``` + ## License [MIT](./LICENSE) @Gumball12 diff --git a/apps/sandbox/src/App.tsx b/apps/sandbox/src/App.tsx index 241c9e0..6a806a8 100644 --- a/apps/sandbox/src/App.tsx +++ b/apps/sandbox/src/App.tsx @@ -20,6 +20,7 @@ type Edits = { secondSep: string; fixationPoint: string; ignoreHtmlTag: string; + ignoreHtmlEntity: string; input: string; }; @@ -28,6 +29,7 @@ const defaultEdits: Edits = { secondSep: '', fixationPoint: '1', ignoreHtmlTag: '1', // 1 = true, 0 = false + ignoreHtmlEntity: '1', // 1 = true, 0 = false input: INITIAL_INPUT, }; @@ -37,6 +39,7 @@ const storeEdits = ({ fixationPoint, input, ignoreHtmlTag, + ignoreHtmlEntity, }: Edits) => { const search = [ `firstSep=${encodeURIComponent(firstSep)}`, @@ -44,6 +47,7 @@ const storeEdits = ({ `fixationPoint=${encodeURIComponent(fixationPoint)}`, `input=${encodeURIComponent(input)}`, `ignoreHtmlTag=${encodeURIComponent(ignoreHtmlTag)}`, + `ignoreHtmlEntity=${encodeURIComponent(ignoreHtmlEntity)}`, ].join('&'); // eslint-disable-next-line @@ -100,6 +104,7 @@ type Action = { | 'HIGHLIGHTED_TEXT' | 'COPIED' | 'TOGGLE_IGNORE_HTML_TAG' + | 'TOGGLE_IGNORE_HTML_ENTITY' | 'RESET'; value: string; copied: boolean; @@ -135,6 +140,11 @@ const reducer: Reducer = (state, { type, value, copied }) => { return { ...state, ignoreHtmlTag: nextIgnoreHtmlTag }; } + if (type === 'TOGGLE_IGNORE_HTML_ENTITY') { + const nextIgnoreHtmlEntity = state.ignoreHtmlEntity === '1' ? '0' : '1'; + return { ...state, ignoreHtmlEntity: nextIgnoreHtmlEntity }; + } + if (type === 'RESET') { return { ...defaultEdits, @@ -161,6 +171,7 @@ const App = () => { copiedEffect, highlightedText, ignoreHtmlTag, + ignoreHtmlEntity, } = state; useEffect(() => { @@ -169,6 +180,7 @@ const App = () => { sep: [firstSep, secondSep], fixationPoint: parseInt(fixationPoint), ignoreHtmlTag: ignoreHtmlTag === '1', + ignoreHtmlEntity: ignoreHtmlEntity === '1', }; const highlightedText = textVide(input, options); @@ -185,11 +197,19 @@ const App = () => { input, fixationPoint, ignoreHtmlTag, + ignoreHtmlEntity, }); }, DEBOUNCE_TIMEOUT); return () => clearTimeout(store); - }, [firstSep, secondSep, input, fixationPoint, ignoreHtmlTag]); + }, [ + firstSep, + secondSep, + input, + fixationPoint, + ignoreHtmlTag, + ignoreHtmlEntity, + ]); const copyUrl = () => { const { href: url } = location; @@ -217,6 +237,7 @@ const App = () => { fixationPoint, input, ignoreHtmlTag, + ignoreHtmlEntity, }); return ( @@ -265,19 +286,9 @@ const App = () => { InputLabelProps={{ shrink: true }} /> - - -
+
{ -
- -
- +
+ +
+
diff --git a/packages/text-vide/src/__tests__/getOptions.test.ts b/packages/text-vide/src/__tests__/getOptions.test.ts index 252aba1..8a2906a 100644 --- a/packages/text-vide/src/__tests__/getOptions.test.ts +++ b/packages/text-vide/src/__tests__/getOptions.test.ts @@ -8,6 +8,7 @@ describe('test getOptions()', () => { sep: ['', ''], fixationPoint: 1, ignoreHtmlTag: true, + ignoreHtmlEntity: true, }; expect(getOptions({})).toEqual(expected); @@ -18,12 +19,14 @@ describe('test getOptions()', () => { sep: undefined, fixationPoint: undefined, ignoreHtmlTag: undefined, + ignoreHtmlEntity: undefined, }; const expected: Options = { sep: ['', ''], fixationPoint: 1, ignoreHtmlTag: true, + ignoreHtmlEntity: true, }; expect(getOptions(undefinedOptionValues)).toEqual(expected); @@ -34,12 +37,14 @@ describe('test getOptions()', () => { sep: ['', ''], fixationPoint: undefined, ignoreHtmlTag: undefined, + ignoreHtmlEntity: undefined, }; const expected: Options = { sep: ['', ''], fixationPoint: 1, ignoreHtmlTag: true, + ignoreHtmlEntity: true, }; expect(getOptions(maybeOptions)).toEqual(expected); @@ -50,6 +55,7 @@ describe('test getOptions()', () => { sep: ['a', 'b'], fixationPoint: 0, // but it's okay ignoreHtmlTag: false, + ignoreHtmlEntity: false, }; expect(getOptions(expected)).toEqual(expected); diff --git a/packages/text-vide/src/__tests__/index.test.ts b/packages/text-vide/src/__tests__/index.test.ts index deef14f..ab3e68f 100644 --- a/packages/text-vide/src/__tests__/index.test.ts +++ b/packages/text-vide/src/__tests__/index.test.ts @@ -158,6 +158,30 @@ describe('test options', () => { expect(textVide(text)).toBe(expected); }); + + it('ignoreHtmlTag: true', () => { + const text = '
abcd
efg'; + const expectedText = '
abcd
efg'; + expect(textVide(text, { ignoreHtmlTag: true })).toBe(expectedText); + }); + + it('ignoreHtmlTag: false', () => { + const text = '
abcd
efg'; + const expected = '<div>abcddiv>efg'; + expect(textVide(text, { ignoreHtmlTag: false })).toBe(expected); + }); + + it('ignoreHtmlEntity: true', () => { + const text = ' abcd>'; + const expectedText = ' abcd>'; + expect(textVide(text, { ignoreHtmlEntity: true })).toBe(expectedText); + }); + + it('ignoreHtmlEntity: false', () => { + const text = ' abcd>'; + const expected = '&nbsp;abcd&gt;'; + expect(textVide(text, { ignoreHtmlEntity: false })).toBe(expected); + }); }); describe('fixation point ([2, 5])', () => { diff --git a/packages/text-vide/src/getOptions.ts b/packages/text-vide/src/getOptions.ts index ef65415..1279f93 100644 --- a/packages/text-vide/src/getOptions.ts +++ b/packages/text-vide/src/getOptions.ts @@ -4,10 +4,12 @@ import defaults from 'utils/defaults'; const DEFAULT_SEP = ['', '']; const DEFAULT_FIXATION_POINT = 1; const DEFAULT_IGNORE_HTML_TAG = true; +const DEFAULT_IGNORE_HTML_ENTITY = true; export default (maybeOptions: Partial): Options => defaults(maybeOptions, { sep: DEFAULT_SEP, fixationPoint: DEFAULT_FIXATION_POINT, ignoreHtmlTag: DEFAULT_IGNORE_HTML_TAG, + ignoreHtmlEntity: DEFAULT_IGNORE_HTML_ENTITY, }); diff --git a/packages/text-vide/src/index.ts b/packages/text-vide/src/index.ts index ff1ee22..ac9fa71 100644 --- a/packages/text-vide/src/index.ts +++ b/packages/text-vide/src/index.ts @@ -3,6 +3,7 @@ import getOptions from './getOptions'; import getFixationLength from './getFixationLength'; import getHighlightedText from './getHighlightedText'; import { useCheckIsHtmlTag } from './useCheckIsHtmlTag'; +import { useCheckIsHtmlEntity } from './useCheckIsHtmlEntity'; const CONVERTIBLE_REGEX = /(\p{L}|\p{Nd})*\p{L}(\p{L}|\p{Nd})*/gu; @@ -11,24 +12,34 @@ export const textVide = (text: string, maybeOptions: Partial = {}) => { return ''; } - const { fixationPoint, sep, ignoreHtmlTag } = getOptions(maybeOptions); - const convertibleMatchList = text.matchAll(CONVERTIBLE_REGEX); + const { fixationPoint, sep, ignoreHtmlTag, ignoreHtmlEntity } = + getOptions(maybeOptions); + const convertibleMatchList = Array.from(text.matchAll(CONVERTIBLE_REGEX)); let result = ''; let lastMatchedIndex = 0; let checkIsHtmlTag: ReturnType | undefined; - if (ignoreHtmlTag) { checkIsHtmlTag = useCheckIsHtmlTag(text); } + let checkIsHtmlEntity: ReturnType | undefined; + if (ignoreHtmlEntity) { + checkIsHtmlEntity = useCheckIsHtmlEntity(text); + } + for (const match of convertibleMatchList) { const isHtmlTag = checkIsHtmlTag?.(match); if (isHtmlTag) { continue; } + const isHtmlEntity = checkIsHtmlEntity?.(match); + if (isHtmlEntity) { + continue; + } + const [matchedWord] = match; const startIndex = match.index!; const endIndex = startIndex + getFixationLength(matchedWord, fixationPoint); diff --git a/packages/text-vide/src/types.ts b/packages/text-vide/src/types.ts index d045992..ff66c4a 100644 --- a/packages/text-vide/src/types.ts +++ b/packages/text-vide/src/types.ts @@ -2,4 +2,5 @@ export type Options = { sep: string | string[]; // default: ['', ''] fixationPoint: number; // default: 1 ignoreHtmlTag: boolean; // default: true + ignoreHtmlEntity: boolean; // default: true }; diff --git a/packages/text-vide/src/useCheckIsHtmlEntity.ts b/packages/text-vide/src/useCheckIsHtmlEntity.ts new file mode 100644 index 0000000..d291d41 --- /dev/null +++ b/packages/text-vide/src/useCheckIsHtmlEntity.ts @@ -0,0 +1,24 @@ +import { extractMatchRangeList } from './utils'; + +const HTML_ENTITY_REGEX = /(&[\w#]+;)/g; + +export const useCheckIsHtmlEntity = (text: string) => { + const htmlEntityMatchList = text.matchAll(HTML_ENTITY_REGEX); + const htmlEntityRangeList = extractMatchRangeList(htmlEntityMatchList); + const reversedHtmlEntityRangeList = htmlEntityRangeList.reverse(); + + return (match: RegExpMatchArray) => { + const startIndex = match.index!; + const entityRange = reversedHtmlEntityRangeList.find( + ([rangeStart]) => startIndex > rangeStart, + ); + + if (!entityRange) { + return false; + } + + const [, rangeEnd] = entityRange; + const isInclude = startIndex < rangeEnd; + return isInclude; + }; +}; diff --git a/packages/text-vide/src/useCheckIsHtmlTag.ts b/packages/text-vide/src/useCheckIsHtmlTag.ts index 74e8688..3abed67 100644 --- a/packages/text-vide/src/useCheckIsHtmlTag.ts +++ b/packages/text-vide/src/useCheckIsHtmlTag.ts @@ -1,8 +1,10 @@ +import { extractMatchRangeList } from './utils'; + const HTML_TAG_REGEX = /()|(<[^>]*>)/g; export const useCheckIsHtmlTag = (text: string) => { const htmlTagMatchList = text.matchAll(HTML_TAG_REGEX); - const htmlTagRangeList = getHtmlTagRangeList(htmlTagMatchList); + const htmlTagRangeList = extractMatchRangeList(htmlTagMatchList); const reversedHtmlTagRangeList = htmlTagRangeList.reverse(); return (match: RegExpMatchArray) => { @@ -20,14 +22,3 @@ export const useCheckIsHtmlTag = (text: string) => { return isInclude; }; }; - -const getHtmlTagRangeList = ( - htmlTagMatchList: IterableIterator, -) => - [...htmlTagMatchList].map(htmlTagMatch => { - const startIndex = htmlTagMatch.index!; - const [tag] = htmlTagMatch; - const { length: tagLength } = tag; - - return [startIndex, startIndex + tagLength - 1]; - }); diff --git a/packages/text-vide/src/utils.ts b/packages/text-vide/src/utils.ts new file mode 100644 index 0000000..f74e9b1 --- /dev/null +++ b/packages/text-vide/src/utils.ts @@ -0,0 +1,10 @@ +export const extractMatchRangeList = ( + matchList: IterableIterator, +) => + Array.from(matchList).map(match => { + const startIndex = match.index!; + const [matchedWord] = match; + const { length: matchedWordLength } = matchedWord; + + return [startIndex, startIndex + matchedWordLength - 1]; + });