diff --git a/README.md b/README.md index cea70b1..4946504 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,13 @@ An Open-Source JavaScript Implementation of [Bionic Reading API](https://bionic- | [Support ESM and CommonJS](#usage) | ✅ | | [Custom `sep` Style](#options-sep) | ✅ | | [Fixation-Points](#options-fixationpoint) | ✅ | +| [Ignore HTML Tags](#options-ignorehtmltag) | ✅ | ### Work in Progress -| Feature | Issues | -| ---------------------------------------------------- | ------------------------------------------------------- | -| Saccade | [#21](https://github.com/Gumball12/text-vide/issues/21) | -| Apply the Bionic Reading technique without HTML code | [#36](https://github.com/Gumball12/text-vide/issues/36) | +| Feature | Issues | +| ------- | ------------------------------------------------------- | +| Saccade | [#21](https://github.com/Gumball12/text-vide/issues/21) | ## ⚙️ Install @@ -85,6 +85,7 @@ textVide('text-vide', { type Options = Partial<{ sep: string | string[]; fixationPoint: number; + ignoreHtmlTag: boolean; }>; ``` @@ -117,6 +118,17 @@ textVide('text-vide'); // 'text-vide' textVide('text-vide', { fixationPoint: 5 }); // 'text-vide' ``` +#### `ignoreHtmlTag` + +- Default Value: `true` + +If this option is `true`, HTML tags are not highlighted. + +```ts +textVite('
abcd
efg'); // '
abcd
efg' +textVite('
abcd
efg', { ignoreHtmlTag: false }); // '<div>abcddiv>efg' +``` + ## License [MIT](./LICENSE) @Gumball12 diff --git a/apps/sandbox/src/App.tsx b/apps/sandbox/src/App.tsx index 12ff33c..03f8b50 100644 --- a/apps/sandbox/src/App.tsx +++ b/apps/sandbox/src/App.tsx @@ -19,6 +19,7 @@ type Edits = { firstSep: string; secondSep: string; fixationPoint: string; + ignoreHtmlTag: string; input: string; }; @@ -26,15 +27,23 @@ const defaultEdits: Edits = { firstSep: '', secondSep: '', fixationPoint: '1', + ignoreHtmlTag: '1', // 1 = true, 0 = false input: INITIAL_INPUT, }; -const storeEdits = ({ firstSep, secondSep, fixationPoint, input }: Edits) => { +const storeEdits = ({ + firstSep, + secondSep, + fixationPoint, + input, + ignoreHtmlTag, +}: Edits) => { const search = [ `firstSep=${encodeURIComponent(firstSep)}`, `secondSep=${encodeURIComponent(secondSep)}`, `fixationPoint=${encodeURIComponent(fixationPoint)}`, `input=${encodeURIComponent(input)}`, + `ignoreHtmlTag=${encodeURIComponent(ignoreHtmlTag)}`, ].join('&'); // eslint-disable-next-line @@ -90,6 +99,7 @@ type Action = { | 'INPUT' | 'HIGHLIGHTED_TEXT' | 'COPIED' + | 'TOGGLE_IGNORE_HTML_TAG' | 'RESET'; value: string; copied: boolean; @@ -120,6 +130,11 @@ const reducer: Reducer = (state, { type, value, copied }) => { return { ...state, copiedEffect: copied }; } + if (type === 'TOGGLE_IGNORE_HTML_TAG') { + const nextIgnoreHtmlTag = state.ignoreHtmlTag === '1' ? '0' : '1'; + return { ...state, ignoreHtmlTag: nextIgnoreHtmlTag }; + } + if (type === 'RESET') { return { ...defaultEdits, @@ -145,6 +160,7 @@ const App = () => { fixationPoint, copiedEffect, highlightedText, + ignoreHtmlTag, } = state; useEffect(() => { @@ -152,6 +168,7 @@ const App = () => { const options = { sep: [firstSep, secondSep], fixationPoint: parseInt(fixationPoint), + ignoreHtmlTag: ignoreHtmlTag === '1', }; const highlightedText = textVide(input, options); @@ -167,11 +184,12 @@ const App = () => { secondSep, input, fixationPoint, + ignoreHtmlTag, }); }, DEBOUNCE_TIMEOUT); return () => clearTimeout(store); - }, [firstSep, secondSep, input, fixationPoint]); + }, [firstSep, secondSep, input, fixationPoint, ignoreHtmlTag]); const copyUrl = () => { const { href: url } = location; @@ -198,6 +216,7 @@ const App = () => { secondSep, fixationPoint, input, + ignoreHtmlTag, }); return ( @@ -276,6 +295,23 @@ const App = () => { 5 + + + +
+ +
diff --git a/packages/text-vide/src/__tests__/getOptions.test.ts b/packages/text-vide/src/__tests__/getOptions.test.ts index e22090a..252aba1 100644 --- a/packages/text-vide/src/__tests__/getOptions.test.ts +++ b/packages/text-vide/src/__tests__/getOptions.test.ts @@ -1,48 +1,55 @@ import { describe, expect, it } from 'vitest'; import getOptions from '../getOptions'; +import { Options } from '../types'; describe('test getOptions()', () => { it('pass empty object', () => { - const expected = { + const expected: Options = { sep: ['', ''], fixationPoint: 1, + ignoreHtmlTag: true, }; expect(getOptions({})).toEqual(expected); }); it('pass undefined value', () => { - const maybeOptions = { + const undefinedOptionValues = { sep: undefined, fixationPoint: undefined, + ignoreHtmlTag: undefined, }; - const expected = { + const expected: Options = { sep: ['', ''], fixationPoint: 1, + ignoreHtmlTag: true, }; - expect(getOptions(maybeOptions)).toEqual(expected); + expect(getOptions(undefinedOptionValues)).toEqual(expected); }); it('pass empty string value', () => { const maybeOptions = { sep: ['', ''], fixationPoint: undefined, + ignoreHtmlTag: undefined, }; - const expected = { + const expected: Options = { sep: ['', ''], fixationPoint: 1, + ignoreHtmlTag: true, }; expect(getOptions(maybeOptions)).toEqual(expected); }); it('pass valid value', () => { - const expected = { + const expected: Options = { sep: ['a', 'b'], fixationPoint: 0, // but it's okay + ignoreHtmlTag: 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 fb62b5c..deef14f 100644 --- a/packages/text-vide/src/__tests__/index.test.ts +++ b/packages/text-vide/src/__tests__/index.test.ts @@ -226,48 +226,124 @@ describe('numbers', () => { it('1234567890', () => { const text = '1234567890'; const expected = '1234567890'; - expect(textVide(text), expected); + expect(textVide(text)).toBe(expected); }); it('1234-567890', () => { const text = '1234-567890'; const expected = '1234-567890'; - expect(textVide(text), expected); + expect(textVide(text)).toBe(expected); }); it('a1234567890', () => { const text = 'a1234567890'; const expected = 'a1234567890'; - expect(textVide(text), expected); + expect(textVide(text)).toBe(expected); }); it('1234567890a', () => { const text = '1234567890a'; const expected = '1234567890a'; - expect(textVide(text), expected); + expect(textVide(text)).toBe(expected); }); it('1234a567890', () => { const text = '1234a567890'; const expected = '1234a567890'; - expect(textVide(text), expected); + expect(textVide(text)).toBe(expected); }); it('@1234567890', () => { const text = '@1234567890'; const expected = '@1234567890'; - expect(textVide(text), expected); + expect(textVide(text)).toBe(expected); }); it('1234567890@', () => { const text = '1234567890@'; const expected = '1234567890@'; - expect(textVide(text), expected); + expect(textVide(text)).toBe(expected); }); it('1234@567890', () => { const text = '1234@567890'; const expected = '1234@567890'; - expect(textVide(text), expected); + expect(textVide(text)).toBe(expected); + }); +}); + +describe('with html tags', () => { + it('normal text', () => { + const text = 'abcdefg'; + const expected = 'abcdefg'; + expect(textVide(text)).toBe(expected); + }); + + it('with a tag', () => { + const text = 'abcdefg'; + const expected = 'abcdefg'; + expect(textVide(text)).toBe(expected); + }); + + it('with b tag', () => { + const text = 'abcdefg'; + const expected = 'abcdefg'; + expect(textVide(text)).toBe(expected); + }); + + it('with div tag', () => { + const text = '
abcd
efg'; + const expected = '
abcd
efg'; + expect(textVide(text)).toBe(expected); + }); + + it('complex html tags', () => { + const text = `
+ + + normal text: abcdefg
with a tag: abcdefg
with b tag: abcdefg
with div tag:
abcd
efg
+ + + +
`; + + const expected = `
+ + + normal text: abcdefg
with a tag: abcdefg
with b tag: abcdefg
with div tag:
abcd
efg
+ + + +
`; + + expect(textVide(text)).toBe(expected); }); }); diff --git a/packages/text-vide/src/getOptions.ts b/packages/text-vide/src/getOptions.ts index 01823e0..ef65415 100644 --- a/packages/text-vide/src/getOptions.ts +++ b/packages/text-vide/src/getOptions.ts @@ -3,9 +3,11 @@ import defaults from 'utils/defaults'; const DEFAULT_SEP = ['', '']; const DEFAULT_FIXATION_POINT = 1; +const DEFAULT_IGNORE_HTML_TAG = true; export default (maybeOptions: Partial): Options => defaults(maybeOptions, { sep: DEFAULT_SEP, fixationPoint: DEFAULT_FIXATION_POINT, + ignoreHtmlTag: DEFAULT_IGNORE_HTML_TAG, }); diff --git a/packages/text-vide/src/index.ts b/packages/text-vide/src/index.ts index 37f032a..2c6efce 100644 --- a/packages/text-vide/src/index.ts +++ b/packages/text-vide/src/index.ts @@ -2,6 +2,7 @@ import { Options } from './types'; import getOptions from './getOptions'; import getFixationLength from './getFixationLength'; import getHighlightedText from './getHighlightedText'; +import { useCheckIsHtmlTag } from './useCheckIsHtmlTag'; const CONVERTIBLE_REGEX = /(\p{L}|\p{Nd})*\p{L}(\p{L}|\p{Nd})*/gu; @@ -10,13 +11,24 @@ export const textVide = (text: string, maybeOptions: Partial = {}) => { return ''; } - const { fixationPoint, sep } = getOptions(maybeOptions); + const { fixationPoint, sep, ignoreHtmlTag } = getOptions(maybeOptions); const convertibleMatchList = text.matchAll(CONVERTIBLE_REGEX); let result = ''; let lastMatchedIndex = 0; + let checkIsHtmlTag: ReturnType | undefined; + + if (ignoreHtmlTag) { + checkIsHtmlTag = useCheckIsHtmlTag(text); + } + for (const match of convertibleMatchList) { + const isHtmlTag = checkIsHtmlTag?.(match); + if (isHtmlTag) { + continue; + } + const startIndex = match.index!; const endIndex = startIndex + getFixationLength(match[0], fixationPoint); diff --git a/packages/text-vide/src/types.ts b/packages/text-vide/src/types.ts index e68254f..d045992 100644 --- a/packages/text-vide/src/types.ts +++ b/packages/text-vide/src/types.ts @@ -1,4 +1,5 @@ export type Options = { - sep: string | string[]; - fixationPoint: number; + sep: string | string[]; // default: ['', ''] + fixationPoint: number; // default: 1 + ignoreHtmlTag: boolean; // default: true }; diff --git a/packages/text-vide/src/useCheckIsHtmlTag.ts b/packages/text-vide/src/useCheckIsHtmlTag.ts new file mode 100644 index 0000000..74e8688 --- /dev/null +++ b/packages/text-vide/src/useCheckIsHtmlTag.ts @@ -0,0 +1,33 @@ +const HTML_TAG_REGEX = /()|(<[^>]*>)/g; + +export const useCheckIsHtmlTag = (text: string) => { + const htmlTagMatchList = text.matchAll(HTML_TAG_REGEX); + const htmlTagRangeList = getHtmlTagRangeList(htmlTagMatchList); + const reversedHtmlTagRangeList = htmlTagRangeList.reverse(); + + return (match: RegExpMatchArray) => { + const startIndex = match.index!; + const tagRange = reversedHtmlTagRangeList.find( + ([rangeStart]) => startIndex > rangeStart, + ); + + if (!tagRange) { + return false; + } + + const [, rangeEnd] = tagRange; + const isInclude = startIndex < rangeEnd; + 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]; + });