Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement text search #123

Merged
merged 9 commits into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# build stage
FROM node:14.5-alpine as build-stage
FROM node:14.5-stretch-slim as build-stage

ARG PUBLIC_URL=/

RUN apk --no-cache add autoconf automake libtool make tiff jpeg zlib zlib-dev pkgconf nasm file gcc musl-dev

WORKDIR /app
COPY package.json yarn.lock /app/
RUN yarn install
Expand Down
4 changes: 1 addition & 3 deletions Dockerfile-dev
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# build stage
FROM node:14.5-alpine as build-stage
FROM node:14.5-stretch-slim as build-stage

ARG PUBLIC_URL=/

RUN apk --no-cache add autoconf automake libtool make tiff jpeg zlib zlib-dev pkgconf nasm file gcc musl-dev

WORKDIR /app
COPY package.json yarn.lock /app/
RUN yarn install
Expand Down
2 changes: 0 additions & 2 deletions build/loaders/lang-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ module.exports = function (source) {
const standardParser = parseStandardFile.bind(this)
const eventParser = parseEventFile.bind(this)

// Eval is safe here since we're getting things directly from the JSON "loader"
// eslint-disable-next-line no-eval
const messages = typeof source === 'string' ? JSON.parse(source) : source

messages.events = build(lang, 'events', eventParser)
Expand Down
148 changes: 148 additions & 0 deletions build/loaders/search-index-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
const fs = require('fs')
const lunr = require('lunr')
require('lunr-languages/lunr.stemmer.support')(lunr)

lunr.tokenizer.separator = /[\s-[\](){}]+/

function removeDuplicates (token, index, tokens) {
const [position] = token.metadata.position

for (let i = index - 1; i >= 0 && tokens[i].metadata.position[0] === position; i--) {
if (tokens[i].toString() === token.toString()) {
return null
}
}

return token
}

lunr.Pipeline.registerFunction(removeDuplicates, 'remove-duplicates')

function nGramTokenizer (obj, metadata) {
if (obj === null || obj === undefined) {
return []
}

if (Array.isArray(obj)) {
return obj.map(function (t) {
return new lunr.Token(
lunr.utils.asString(t).toLowerCase(),
lunr.utils.clone(metadata)
)
})
}

const str = obj.toString().toLowerCase()
const len = str.length
const tokens = []

for (let sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) {
const char = str.charAt(sliceEnd)
const sliceLength = sliceEnd - sliceStart

if ((char.match(lunr.tokenizer.separator) || sliceEnd === len)) {
if (sliceLength > 0) {
const tokenMetadata = lunr.utils.clone(metadata) || {}
tokenMetadata.position = [sliceStart, sliceLength]
tokenMetadata.index = tokens.length

const baseToken = str.slice(sliceStart, sliceEnd)
if (baseToken.length <= 3) {
tokens.push(new lunr.Token(baseToken, tokenMetadata))
} else {
for (let i = 3; i <= baseToken.length; i++) {
const meta = lunr.utils.clone(tokenMetadata)
meta.position = [sliceStart, i]
meta.index = tokens.length

tokens.push(new lunr.Token(baseToken.slice(0, i), meta))
}
}
}

sliceStart = sliceEnd + 1
}
}

return tokens
}

module.exports = function (source) {
if (this.cacheable) {
this.cacheable()
}

this.async()

const messages = typeof source === 'string' ? JSON.parse(source) : source
const load = async (key) => {
const path = await new Promise((resolve, reject) => this.resolve(
this.rootContext,
`@/store/${key}.json`,
(error, result) => error !== null ? reject(error) : resolve(result)
))
this.addDependency(path)
return JSON.parse(fs.readFileSync(path)).reduce(
(acc, entry) => {
acc[entry.id] = entry
return acc
},
{}
)
}

async function buildIndex () {
const searchable = (await Promise.all(['events', 'locations', 'characters', 'misc'].map(async key => ({
key,
value: await load(key)
})))).reduce(
(acc, entry) => {
acc[entry.key] = entry.value
return acc
},
{}
)
return lunr(function () {
const lunrLanguage = messages['search-language']
this.tokenizer = nGramTokenizer
if (lunrLanguage !== 'en') {
require(`lunr-languages/lunr.${lunrLanguage}`)(lunr)
this.use(lunr[lunrLanguage])
}

this.pipeline.add(removeDuplicates)

this.ref('id')
this.field('name', { boost: 10 })
this.field('details')
this.field('artist')

Object.entries(searchable).forEach(([entryType, entryData]) => {
const entries = messages[entryType] ?? []
Object.keys(entries).forEach((id) => {
const entry = entryData[id]
let artist
if (entry !== undefined && entry.image !== undefined && entry.image.credits !== undefined) {
const markdownResult = /^\[([^\]]+)]\(.*\)$/.exec(entry.image.credits)
artist = markdownResult !== null ? markdownResult[1] : entry.image.credits
}
this.add(
{ ...entries[id], id: `${entryType}/${id}`, artist },
{ boost: entryType !== 'events' ? 2 : 1 }
)
})
})
})
}

buildIndex()
.then(index =>
this.callback(
null,
JSON.stringify(index)
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029')
)
)
.catch(error => this.callback(error))
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"hammerjs": "^2.0.8",
"is-mobile": "^2.2.2",
"jszip": "^3.7.0",
"lunr": "^2.3.9",
"lunr-languages": "^1.8.0",
"register-service-worker": "^1.7.1",
"seedrandom": "^3.0.5",
"simple-markdown": "^0.7.2",
Expand Down Expand Up @@ -53,6 +55,7 @@
"imagemin-webp": "^6.0.0",
"imagemin-zopfli": "^7.0.0",
"js-string-escape": "^1.0.1",
"nodejieba": "^2.5.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.11",
Expand Down
110 changes: 105 additions & 5 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,27 @@
<transition name="scrubber" duration="1500" @after-enter="onScrubberLoaded">
<Scrubber v-if="ready" />
</transition>
<Info @open="sidebarActive = true" @open-tutorial="tutorialActive = true" @close="sidebarActive = false" />
<Settings @open="sidebarActive = true" @close="sidebarActive = false" />
<div class="app__actions">
<Search :open="openedMenu === 'search'" @open="openMenu('search')" @close="closeMenu" />
<button
data-tutorial-id="settings-button"
:class="['app__actions-button', 'app__actions-button--wide', {'app__actions-button--hidden': openedMenu === 'settings'}]"
@click="openMenu('settings')"
>
<SlidersIcon size="1x" />
{{ $t('ui.settings') }}
</button>
<button
data-tutorial-id="menu-button"
:class="['app__actions-button', {'app__actions-button--hidden': openedMenu === 'info'}]"
:title="$t('ui.menu')"
@click="openMenu('info')"
>
<MenuIcon size="1x" />
</button>
</div>
<Info :open="openedMenu === 'info'" @open-tutorial="tutorialActive = true" @close="closeMenu" />
<Settings :open="openedMenu === 'settings'" @close="closeMenu" />
<transition name="calendar-guide">
<CalendarGuide v-if="$store.state.calendarGuideOpen" />
</transition>
Expand All @@ -32,6 +51,7 @@
</template>

<script>
import { MenuIcon, SlidersIcon } from 'vue-feather-icons'
import Scrubber from '@/components/Scrubber.vue'
import Settings from '@/components/Settings.vue'
import LoadingIndicator from '@/components/LoadingIndicator.vue'
Expand All @@ -42,10 +62,13 @@ import Tutorial from '@/components/Tutorial.vue'
import FirstVisitWindow from '@/components/FirstVisitWindow.vue'
import ErrorScreen from '@/components/ErrorScreen.vue'
import '@/assets/fonts/hebrew.scss'
import Search from '@/components/search/Search.vue'
import { mapMutations, mapState } from 'vuex'

export default {
name: 'App',
components: {
Search,
ErrorScreen,
FirstVisitWindow,
Tutorial,
Expand All @@ -54,14 +77,15 @@ export default {
Info,
LoadingIndicator,
Settings,
Scrubber
Scrubber,
MenuIcon,
SlidersIcon
},
data () {
return {
ready: false,
errored: false,
mapTransitions: false,
sidebarActive: false,
tutorialActive: window.localStorage.tutorialStarted === 'true' && window.localStorage.tutorialDone !== 'true',
firstVisit: window.localStorage.tutorialStarted !== 'true' && window.localStorage.tutorialDone !== 'true'
}
Expand All @@ -75,6 +99,10 @@ export default {
}

return null
},
...mapState(['openedMenu']),
sidebarActive () {
return this.openedMenu === 'settings' || this.openedMenu === 'info'
}
},
watch: {
Expand Down Expand Up @@ -113,7 +141,8 @@ export default {
},
onScrubberLoaded () {
this.mapTransitions = true
}
},
...mapMutations(['openMenu', 'closeMenu'])
}
}
</script>
Expand Down Expand Up @@ -174,6 +203,77 @@ body {
padding-left: 225px;
}
}

.app__actions {
position: fixed;
top: 2rem;
display: flex;
grid-gap: 1rem;
max-width: calc(100% - 4rem);
z-index: 60;

[dir=ltr] & {
right: 2rem;
}

[dir=rtl] & {
left: 2rem;
}

&-button {
display: flex;
align-items: center;
position: relative;
font-size: 1rem;
line-height: 1;
appearance: none;
outline: none;
box-sizing: border-box;
border: none;
z-index: 61;
background: #F5ECDA;
border-radius: 2rem;
padding: 0.75rem 0.75rem;
cursor: pointer;
transition: all 0.2s ease-in-out;
color: #242629;
pointer-events: auto;
box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.5);

&:hover, &:active, &:focus {
background: saturate(darken(#F5ECDA, 10%), 5%);
}

&--wide {
padding-left: 1.5rem;
padding-right: 1.5rem;
}

&--hidden {
cursor: default !important;
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
pointer-events: none;
opacity: 0;
transform: scale(0);
}

[dir=ltr] & {
transform-origin: calc(100% - 1rem) 50%;

&--wide .feather {
margin-right: 0.5rem;
}
}

[dir=rtl] & {
transform-origin: 1rem 50%;

&--wide .feather {
margin-left: 0.5rem;
}
}
}
}
}

button {
Expand Down
2 changes: 1 addition & 1 deletion src/components/CalendarGuide.vue
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export default {
opacity: 0;
}

&-enter-to, &-leave-from {
&-enter-to, &-leave {
opacity: 1;
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/GoToDate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export default {
}
}

&-enter-to, &-leave-from {
&-enter-to, &-leave {
opacity: 1;

.go-to-date {
Expand Down
Loading