diff --git a/apps/sodo-search/src/App.js b/apps/sodo-search/src/App.js index e4db34bd86..03bd4a5b6b 100644 --- a/apps/sodo-search/src/App.js +++ b/apps/sodo-search/src/App.js @@ -9,20 +9,23 @@ export default class App extends React.Component { constructor(props) { super(props); - const searchIndex = new SearchIndex({ - adminUrl: props.adminUrl, - apiKey: props.apiKey - }); - const i18nLanguage = this.props.locale || 'en'; const i18n = i18nLib(i18nLanguage, 'search'); + const dir = i18n.dir() || 'ltr'; + + const searchIndex = new SearchIndex({ + adminUrl: props.adminUrl, + apiKey: props.apiKey, + dir: dir + }); this.state = { searchIndex, showPopup: false, indexStarted: false, indexComplete: false, - t: i18n.t + t: i18n.t, + dir: dir }; this.inputRef = React.createRef(); @@ -169,7 +172,8 @@ export default class App extends React.Component { }); } }, - t: this.state.t + t: this.state.t, + dir: this.state.dir }}> diff --git a/apps/sodo-search/src/AppContext.js b/apps/sodo-search/src/AppContext.js index 420758711d..801ef02550 100644 --- a/apps/sodo-search/src/AppContext.js +++ b/apps/sodo-search/src/AppContext.js @@ -14,7 +14,8 @@ const AppContext = React.createContext({ searchIndex: null, indexComplete: false, searchValue: '', - t: () => {} + t: () => {}, + dir: 'ltr' }); export default AppContext; diff --git a/apps/sodo-search/src/components/Frame.js b/apps/sodo-search/src/components/Frame.js index 3f76de9a21..e2308a0596 100644 --- a/apps/sodo-search/src/components/Frame.js +++ b/apps/sodo-search/src/components/Frame.js @@ -17,6 +17,8 @@ export default class Frame extends Component { setupFrameBaseStyle() { if (this.node.contentDocument) { this.iframeHtml = this.node.contentDocument.documentElement; + // set the iframeHtml dir attribute + this.iframeHtml.setAttribute('dir', this.props.searchdir); this.iframeHead = this.node.contentDocument.head; this.iframeRoot = this.node.contentDocument.body; this.forceUpdate(); diff --git a/apps/sodo-search/src/components/PopupModal.js b/apps/sodo-search/src/components/PopupModal.js index 832420e881..abd3ec3b64 100644 --- a/apps/sodo-search/src/components/PopupModal.js +++ b/apps/sodo-search/src/components/PopupModal.js @@ -100,7 +100,7 @@ function SearchBox() { return (
-
+
@@ -158,7 +158,7 @@ function CancelButton() { return (
); @@ -433,11 +433,11 @@ function AuthorAvatar({name, avatar}) { const Character = name.charAt(0); if (Avatar) { return ( - {name}/ + {name}/ ); } return ( -
{Character}
+
{Character}
); } @@ -672,7 +672,7 @@ export default class PopupModal extends React.Component { return (
- +
this.handlePopupClose(e)} className='absolute top-0 bottom-0 left-0 right-0 block backdrop-blur-[2px] animate-fadein z-0 bg-gradient-to-br from-[rgba(0,0,0,0.2)] to-[rgba(0,0,0,0.1)]' /> diff --git a/apps/sodo-search/src/search-index.js b/apps/sodo-search/src/search-index.js index 89f9dcf363..6b9967bde6 100644 --- a/apps/sodo-search/src/search-index.js +++ b/apps/sodo-search/src/search-index.js @@ -2,15 +2,17 @@ import Flexsearch from 'flexsearch'; import GhostContentAPI from '@tryghost/content-api'; export default class SearchIndex { - constructor({adminUrl, apiKey}) { + constructor({adminUrl, apiKey, dir}) { this.api = new GhostContentAPI({ url: adminUrl, key: apiKey, version: 'v5.0' }); - + const rtl = (dir === 'rtl'); + const tokenize = (dir === 'rtl') ? 'reverse' : 'forward'; this.postsIndex = new Flexsearch.Document({ - tokenize: 'forward', + tokenize: tokenize, + rtl: rtl, document: { id: 'id', index: ['title', 'excerpt'], @@ -19,7 +21,8 @@ export default class SearchIndex { ...this.#getEncodeOptions() }); this.authorsIndex = new Flexsearch.Document({ - tokenize: 'forward', + tokenize: tokenize, + rtl: rtl, document: { id: 'id', index: ['name'], @@ -28,7 +31,8 @@ export default class SearchIndex { ...this.#getEncodeOptions() }); this.tagsIndex = new Flexsearch.Document({ - tokenize: 'forward', + tokenize: tokenize, + rtl: rtl, document: { id: 'id', index: ['name'], diff --git a/apps/sodo-search/src/search-index.test.js b/apps/sodo-search/src/search-index.test.js index d76ffd4626..99f9f06944 100644 --- a/apps/sodo-search/src/search-index.test.js +++ b/apps/sodo-search/src/search-index.test.js @@ -72,7 +72,7 @@ describe('search index', function () { url: 'http://localhost/ghost/tags/barcelona-tag/' }] }); - + await searchIndex.init(); let searchResults = searchIndex.search('Barcelo'); @@ -93,5 +93,164 @@ describe('search index', function () { expect(searchResults.posts.length).toEqual(0); expect(searchResults.authors.length).toEqual(0); expect(searchResults.tags.length).toEqual(0); + + // confirms that search works in the forward direction for ltr languages: + let searchWithStartResults = searchIndex.search('Barce'); + expect(searchWithStartResults.posts.length).toEqual(1); + + let searchWithEndResults = searchIndex.search('celona'); + expect(searchWithEndResults.posts.length).toEqual(0); + }); + + test('searching works when dir = rtl also', async () => { + const adminUrl = 'http://localhost:3000'; + const apiKey = '69010382388f9de5869ad6e558'; + const searchIndex = new SearchIndex({adminUrl, apiKey, dir: 'ltr', storage: localStorage}); + + nock('http://localhost:3000/ghost/api/content') + .get('/posts/?key=69010382388f9de5869ad6e558&limit=10000&fields=id%2Cslug%2Ctitle%2Cexcerpt%2Curl%2Cupdated_at%2Cvisibility&order=updated_at%20DESC') + .reply(200, { + posts: [{ + id: 'sounique', + title: 'أُظهر المثابرة كل يوم', + excerpt: 'أظهر المثابرة كل يوم. كتابة الاختبارات تحدٍ كبير!', + url: 'http://localhost/ghost/awesome-barcelona-life/' + }, + { + id: 'sounique2', + title: 'هذا منشور عن السعادة', + excerpt: 'هذا منشور عن السعادة. لا يتطابق مع استعلام البحث.', + url: 'http://localhost/ghost/awesome-barcelona-life2/' + }] + }) + .get('/authors/?key=69010382388f9de5869ad6e558&limit=10000&fields=id,slug,name,url,profile_image&order=updated_at%20DESC') + .reply(200, { + authors: [{ + id: 'different_uniq', + slug: 'barcelona-author', + name: 'اسمي المثابرة', + profile_image: 'https://url_to_avatar/barcelona.png', + url: 'http://localhost/ghost/authors/barcelona-author/' + }, { + id: 'different_uniq_2', + slug: 'bob', + name: 'Bob', + profile_image: 'https://url_to_avatar/barcelona.png', + url: 'http://localhost/ghost/authors/bob/' + }] + }) + .get('/tags/?key=69010382388f9de5869ad6e558&&limit=10000&fields=id,slug,name,url&order=updated_at%20DESC&filter=visibility%3Apublic') + .reply(200, { + tags: [{ + id: 'uniq_tag', + slug: 'barcelona-tag', + name: 'المثابرة', + url: 'http://localhost/ghost/tags/barcelona-tag/' + }] + }); + + await searchIndex.init(); + + let searchResults = searchIndex.search('المثابرة'); + expect(searchResults.posts.length).toEqual(1); + expect(searchResults.posts[0].title).toEqual('أُظهر المثابرة كل يوم'); + expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/awesome-barcelona-life/'); + + expect(searchResults.authors.length).toEqual(1); + expect(searchResults.authors[0].name).toEqual('اسمي المثابرة'); + expect(searchResults.authors[0].url).toEqual('http://localhost/ghost/authors/barcelona-author/'); + expect(searchResults.authors[0].profile_image).toEqual('https://url_to_avatar/barcelona.png'); + + expect(searchResults.tags.length).toEqual(1); + expect(searchResults.tags[0].name).toEqual('المثابرة'); + expect(searchResults.tags[0].url).toEqual('http://localhost/ghost/tags/barcelona-tag/'); + + searchResults = searchIndex.search('Nothing like this'); + expect(searchResults.posts.length).toEqual(0); + expect(searchResults.authors.length).toEqual(0); + expect(searchResults.tags.length).toEqual(0); + + let searchWithStartResults = searchIndex.search('المثا'); + expect(searchWithStartResults.posts.length).toEqual(1); + expect(searchWithStartResults.posts[0].title).toEqual('أُظهر المثابرة كل يوم'); + + let searchWithEndResults = searchIndex.search('ثابرة'); + expect(searchWithEndResults.posts.length).toEqual(0); + }); + + test('searching handles CJK characters correctly', async () => { + const adminUrl = 'http://localhost:3000'; + const apiKey = '69010382388f9de5869ad6e558'; + const searchIndex = new SearchIndex({adminUrl, apiKey, dir: 'ltr', storage: localStorage}); + + nock('http://localhost:3000/ghost/api/content') + .get('/posts/?key=69010382388f9de5869ad6e558&limit=10000&fields=id%2Cslug%2Ctitle%2Cexcerpt%2Curl%2Cupdated_at%2Cvisibility&order=updated_at%20DESC') + .reply(200, { + posts: [{ + id: 'sounique', + title: '接收電子報 Regisztráljon fizetős', + excerpt: '要是系統發送電子報時遇到永久失敗的情形,English 該帳號將停止接收電子報 Regisztráljon fizetős fiókot يتطابق a المثابرة كل يوم hozzásćzólások írásához あなたのリクエストはこのサイトの管理者に送信されます。Пријавете го овој коментар Dołączdo płatnej społeczności {{publication}}, by zaąćcąć komećantować. vietnamese: Yêu cầu nhà cung cấp dịch vụ email hỗ trợ bengali: নিউরো সার্জন', + url: 'http://localhost/ghost/visting-china-as-a-polyglot/' + }, + { + id: 'sounique2', + title: 'هذا منشور عن السعادة', + excerpt: 'هذا منشور عن السعادة. لا يتطابق مع استعلام البحث.', + url: 'http://localhost/ghost/a-post-in-arabic/' + }, + { + id: 'sounique3', + title: '毅力和运气', + excerpt: '凭借运气和毅力,Cathy 将通过所有测试。', + url: 'http://localhost/ghost/a-post-in-chinese/' + }] + }) + .get('/authors/?key=69010382388f9de5869ad6e558&limit=10000&fields=id,slug,name,url,profile_image&order=updated_at%20DESC') + .reply(200, { + authors: [{ + id: 'different_uniq', + slug: 'barcelona-author', + name: 'Barcelona Author', + profile_image: 'https://url_to_avatar/barcelona.png', + url: 'http://localhost/ghost/authors/barcelona-author/' + }, { + id: 'different_uniq_2', + slug: 'bob', + name: 'Bob', + profile_image: 'https://url_to_avatar/barcelona.png', + url: 'http://localhost/ghost/authors/bob/' + }] + }) + .get('/tags/?key=69010382388f9de5869ad6e558&&limit=10000&fields=id,slug,name,url&order=updated_at%20DESC&filter=visibility%3Apublic') + .reply(200, { + tags: [{ + id: 'uniq_tag', + slug: 'barcelona-tag', + name: 'Barcelona Tag', + url: 'http://localhost/ghost/tags/barcelona-tag/' + }] + }); + + await searchIndex.init(); + + let searchResults = searchIndex.search('Regisztrálj'); + expect(searchResults.posts.length).toEqual(1); + expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/visting-china-as-a-polyglot/'); + + searchResults = searchIndex.search('Nothing like this'); + expect(searchResults.posts.length).toEqual(0); + + searchResults = searchIndex.search('報'); + expect(searchResults.posts.length).toEqual(1); + expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/visting-china-as-a-polyglot/'); + + // out of order Chinese: + searchResults = searchIndex.search('接子收電'); + expect(searchResults.posts.length).toEqual(1); + expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/visting-china-as-a-polyglot/'); + + // out of order English: + searchResults = searchIndex.search('glenish'); + expect(searchResults.posts.length).toEqual(0); }); });