-
Notifications
You must be signed in to change notification settings - Fork 0
add favicon, error page, remove garu tokenizer #13
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
Merged
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import * as Sentry from '@sentry/tanstackstart-react'; | ||
| import { type ErrorComponentProps, useRouterState } from '@tanstack/react-router'; | ||
| import { useEffect, useRef, useState } from 'react'; | ||
| import { localeFromPathname, localizeHref, LocaleProvider, useLocale } from '../lib/locale'; | ||
| import { useUiStrings } from '../lib/ui-strings'; | ||
| import { SiteHeader } from './SiteHeader'; | ||
|
|
||
| // Global error page. Rendered by the router's `defaultErrorComponent`, so it | ||
| // catches anything a route's render or loader throws — including the docs | ||
| // loaders — and, like `NotFound`, takes its locale from the path so a reader who | ||
| // broke on `/ko/...` stays in Korean. | ||
| export function ErrorPage({ error, info, reset }: ErrorComponentProps) { | ||
| const pathname = useRouterState({ select: state => state.location.pathname }); | ||
| const locale = localeFromPathname(pathname); | ||
|
|
||
| return ( | ||
| <LocaleProvider locale={locale}> | ||
| <div className="flex min-h-svh flex-col bg-fd-background text-fd-foreground"> | ||
| <SiteHeader className="sticky top-0" /> | ||
| <main className="flex flex-1 flex-col items-center justify-center px-6 py-24 text-center"> | ||
| <ErrorBody error={error} info={info} reset={reset} /> | ||
| </main> | ||
| </div> | ||
| </LocaleProvider> | ||
| ); | ||
| } | ||
|
|
||
| function ErrorBody({ error, info, reset }: ErrorComponentProps) { | ||
| const t = useUiStrings(); | ||
| const locale = useLocale(); | ||
| const [eventId, setEventId] = useState<string>(); | ||
| const reported = useRef<unknown>(undefined); | ||
|
|
||
| // Report to Sentry once per error instance — the ref survives the re-renders | ||
| // and remounts that `reset` triggers, so a reader retrying a persistent | ||
| // failure doesn't send the same event repeatedly. `info.componentStack` is | ||
| // attached under the `react` context, the same key Sentry's React integration | ||
| // uses, so the stack shows up where the UI expects it. | ||
| // | ||
| // Effects never run during SSR, so this fires on the client only. That is the | ||
| // point: an error the router catches and renders here never propagates out of | ||
| // the server handler, so the Sentry request/function middleware (src/start.ts) | ||
| // never sees it. This is the report. | ||
| useEffect(() => { | ||
| if (reported.current === error) return; | ||
| reported.current = error; | ||
| setEventId( | ||
| Sentry.captureException(error, { | ||
| tags: { boundary: 'router', locale }, | ||
| contexts: { react: { componentStack: info?.componentStack } }, | ||
| }) | ||
| ); | ||
| }, [error, info, locale]); | ||
|
|
||
| return ( | ||
| <> | ||
| <p className="font-mono text-sm font-medium tracking-[0.2em] text-brand uppercase"> | ||
| {t.error.eyebrow} | ||
| </p> | ||
| <h1 className="mt-4 text-3xl font-semibold tracking-tight text-fd-foreground sm:text-4xl"> | ||
| {t.error.title} | ||
| </h1> | ||
| <p className="mt-3 max-w-md text-fd-muted-foreground">{t.error.message}</p> | ||
|
|
||
| {/* The raw message is useful while developing and noise (or a leak) in | ||
| production, where the error ID is what we'd actually ask a reader for. */} | ||
| {import.meta.env.DEV && ( | ||
| <pre className="mt-6 max-w-xl overflow-x-auto rounded-md border border-zinc-300 bg-fd-muted/50 p-4 text-left font-mono text-xs text-fd-muted-foreground dark:border-zinc-800"> | ||
| {error.message} | ||
| </pre> | ||
| )} | ||
|
|
||
| <div className="mt-8 flex flex-wrap items-center justify-center gap-3"> | ||
| <button | ||
| type="button" | ||
| onClick={reset} | ||
| className="inline-flex items-center justify-center gap-2 rounded-md bg-brand px-5 py-2.5 font-mono text-[14px] font-medium text-white transition-colors hover:bg-brand-hover" | ||
| > | ||
| {t.error.retry} | ||
| </button> | ||
| <a | ||
| href={localizeHref('/', locale)} | ||
| className="inline-flex items-center justify-center gap-2 rounded-md border border-zinc-300 px-5 py-2.5 font-mono text-[14px] font-medium text-zinc-800 transition-colors hover:border-zinc-500 dark:border-zinc-800 dark:text-zinc-200 dark:hover:border-zinc-600" | ||
| > | ||
| {t.error.home} | ||
| </a> | ||
| </div> | ||
|
|
||
| {eventId != null && ( | ||
| <p className="mt-8 font-mono text-xs text-fd-muted-foreground"> | ||
| {t.error.eventId}: <span className="select-all">{eventId}</span> | ||
| </p> | ||
| )} | ||
| </> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| import type { Tokenizer } from '@orama/orama'; | ||
| import { tokenizer as oramaTokenizer } from '@orama/orama/components'; | ||
|
|
||
| // A dependency-free Korean tokenizer for Orama. | ||
| // | ||
| // Orama has no `ko` language (its default map would throw "Language 'ko' is not | ||
| // supported"), and a real morphological analyzer (garu-ko) costs ~1MB of WASM + | ||
| // model in the Worker bundle to serve a docs-sized corpus. Two cheap properties | ||
| // replace it, because Korean glues its particles (조사) and endings (어미) onto | ||
| // the *end* of a word: | ||
| // | ||
| // 1. Orama's index is a radix tree and matches by prefix, so an indexed | ||
| // `설치하세요` is already found by the query `설치`, and `번들을` by `번들`. | ||
| // Inflection in the document therefore costs us nothing. | ||
| // 2. The reverse — a reader who *types* the inflected form, `번들을`, looking | ||
| // for a page that says `번들` — is what this tokenizer adds: it strips a | ||
| // trailing particle or ending, so both sides also index the bare stem. | ||
| // | ||
| // Tokens are additive (the raw word is always kept), so stripping can only add | ||
| // recall, never remove a match. | ||
| // | ||
| // Deliberately *not* done: character bigrams (the usual CJK trick). They buy | ||
| // mid-word matches, which Korean docs rarely need since they space their words, | ||
| // and they cost real precision — `오프라인` would rank `파이프라인` first, on the | ||
| // strength of the shared `프라` and `라인`. | ||
|
|
||
| // Precomposed Hangul syllables (가-힣). Jamo-only text (ㄱ, ㅏ) is rare outside | ||
| // IME state and is left to the base tokenizer. | ||
| const HANGUL_RUN = /[가-힣]+/g; | ||
|
|
||
| // Particles and endings, longest-first at build time so `번들에서는` strips | ||
| // `에서는` rather than the shorter `는`. Not exhaustive, and it doesn't need to | ||
| // be: this only has to catch what a reader is likely to type into a search box. | ||
| const SUFFIXES = [ | ||
| // 조사 (particles) | ||
| '으로부터', | ||
| '에서부터', | ||
| '로부터', | ||
| '에게서', | ||
| '한테서', | ||
| '에서는', | ||
| '에서도', | ||
| '에서의', | ||
| '으로서', | ||
| '으로써', | ||
| '으로는', | ||
| '으로도', | ||
| '이라는', | ||
| '이라고', | ||
| '에게는', | ||
| '에게도', | ||
| '에서', | ||
| '에게', | ||
| '한테', | ||
| '께서', | ||
| '으로', | ||
| '까지', | ||
| '부터', | ||
| '처럼', | ||
| '보다', | ||
| '마다', | ||
| '조차', | ||
| '밖에', | ||
| '이나', | ||
| '라는', | ||
| '라고', | ||
| '와는', | ||
| '과는', | ||
| '에는', | ||
| '에도', | ||
| '에만', | ||
| '들의', | ||
| '들을', | ||
| '들이', | ||
| '들은', | ||
| '은', | ||
| '는', | ||
| '이', | ||
| '가', | ||
| '을', | ||
| '를', | ||
| '에', | ||
| '의', | ||
| '와', | ||
| '과', | ||
| '도', | ||
| '만', | ||
| '로', | ||
| '들', | ||
| // 어미 (verb / adjective endings) | ||
| '하였습니다', | ||
| '했습니다', | ||
| '됐습니다', | ||
| '있습니다', | ||
| '없습니다', | ||
| '되었다', | ||
| '합니다', | ||
| '됩니다', | ||
| '입니다', | ||
| '습니다', | ||
| '해주세요', | ||
| '하십시오', | ||
| '하세요', | ||
| '하려면', | ||
| '하는', | ||
| '하고', | ||
| '하며', | ||
| '하면', | ||
| '하여', | ||
| '해서', | ||
| '했다', | ||
| '하지', | ||
| '하기', | ||
| '되는', | ||
| '되고', | ||
| '된다', | ||
| '되면', | ||
| '되어', | ||
| '있는', | ||
| '없는', | ||
| '한다', | ||
| '했던', | ||
| '었다', | ||
| '았다', | ||
| '였다', | ||
| ].sort((a, b) => b.length - a.length); | ||
|
|
||
| // Stems shorter than this are dropped: a 1-character stem (`참고` → `참`) is far | ||
| // more likely to be an over-strip than a useful term. | ||
| const MIN_STEM = 2; | ||
|
|
||
| // The stem of a Hangul word, or undefined when no suffix applies. | ||
| function stem(word: string): string | undefined { | ||
| for (const suffix of SUFFIXES) { | ||
| if (word.length - suffix.length >= MIN_STEM && word.endsWith(suffix)) { | ||
| return word.slice(0, -suffix.length); | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| function koreanTokens(raw: string): string[] { | ||
| const tokens: string[] = []; | ||
| for (const [word] of raw.normalize('NFC').matchAll(HANGUL_RUN)) { | ||
| tokens.push(word); | ||
| const root = stem(word); | ||
| if (root != null) { | ||
| tokens.push(root); | ||
| } | ||
| } | ||
| return tokens; | ||
| } | ||
|
|
||
| // Korean pages here are dense with English API names, code identifiers, and | ||
| // numbers (`.wvb`, `createBundle`, `v1`). Orama's English tokenizer treats | ||
| // Hangul as a separator, so it yields exactly those Latin tokens — stopword | ||
| // filtered — and we union them with the Korean ones. | ||
| export function createKoreanTokenizer(): Tokenizer { | ||
| const base = oramaTokenizer.createTokenizer({ language: 'english' }); | ||
|
|
||
| return { | ||
| language: 'korean', | ||
| normalizationCache: new Map<string, string>(), | ||
| tokenize(raw: string, language?: string, prop?: string, withCache?: boolean): string[] { | ||
| return [...new Set([...base.tokenize(raw, language, prop, withCache), ...koreanTokens(raw)])]; | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| }, | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.