このコミットが含まれているのは:
@@ -34,6 +34,34 @@ class WikiPagesController < ApplicationController
|
|||||||
render json:
|
render json:
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def autolink_candidates
|
||||||
|
locale = resolve_locale!
|
||||||
|
language = params.fetch(:language, locale.language_code)
|
||||||
|
return head :not_found unless language.is_a?(String)
|
||||||
|
target_locale =
|
||||||
|
if language == 'und'
|
||||||
|
locale
|
||||||
|
else
|
||||||
|
Locale.where(language_code: language, deprecated_at: nil,
|
||||||
|
code: Locale.codes.values).order(:code).first
|
||||||
|
end
|
||||||
|
return head :not_found unless target_locale
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
unless language == 'und'
|
||||||
|
names = TagDescription.joins(tag: :tag_names)
|
||||||
|
.where(language_code: language, tag_names: { language_code: language })
|
||||||
|
.pluck('tag_descriptions.tag_id', 'tag_descriptions.id', 'tag_names.name')
|
||||||
|
candidates = names.map do |tag_id, description_id, name|
|
||||||
|
WikiPageRepr.autolink_tag(target_locale, tag_id:, description_id:, name:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
candidates.concat(TagGroup.order(:id).pluck(:id, :name).map do |id, name|
|
||||||
|
WikiPageRepr.autolink_group(target_locale, id:, name:, language:)
|
||||||
|
end)
|
||||||
|
render json: candidates
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
locale = resolve_locale!
|
locale = resolve_locale!
|
||||||
tag_description = TagDescription.find_by(id: params[:id], language_code: locale.language_code)
|
tag_description = TagDescription.find_by(id: params[:id], language_code: locale.language_code)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ module WikiPageRepr
|
|||||||
|
|
||||||
{ 'id' => description.id,
|
{ 'id' => description.id,
|
||||||
'resource_type' => 'tag_description',
|
'resource_type' => 'tag_description',
|
||||||
|
'tag_id' => tag.id,
|
||||||
|
'language_code' => description.language_code,
|
||||||
'tag_name_id' => tag.tag_name(locale.language_code)&.id,
|
'tag_name_id' => tag.tag_name(locale.language_code)&.id,
|
||||||
'body' => description.body,
|
'body' => description.body,
|
||||||
'created_user_id' => description.created_by_user_id,
|
'created_user_id' => description.created_by_user_id,
|
||||||
@@ -24,4 +26,24 @@ module WikiPageRepr
|
|||||||
def tag_group tag_group
|
def tag_group tag_group
|
||||||
TagGroupRepr.tag_group(tag_group).merge('resource_type' => 'tag_group')
|
TagGroupRepr.tag_group(tag_group).merge('resource_type' => 'tag_group')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def autolink_tag locale, tag_id:, description_id:, name:
|
||||||
|
{ resource_type: 'tag_description',
|
||||||
|
resource_id: description_id,
|
||||||
|
tag_id:,
|
||||||
|
tag_description_id: description_id,
|
||||||
|
name:,
|
||||||
|
language_code: locale.language_code,
|
||||||
|
locale_code: locale.code_before_type_cast }
|
||||||
|
end
|
||||||
|
|
||||||
|
def autolink_group locale, id:, name:, language:
|
||||||
|
{ resource_type: 'tag_group',
|
||||||
|
resource_id: id,
|
||||||
|
tag_id: nil,
|
||||||
|
tag_description_id: nil,
|
||||||
|
name: "#{ TagGroupRepr::PREFIX }#{ name }",
|
||||||
|
language_code: language,
|
||||||
|
locale_code: locale.code_before_type_cast }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ Rails.application.routes.draw do
|
|||||||
collection do
|
collection do
|
||||||
get :search, action: :index
|
get :search, action: :index
|
||||||
get :changes
|
get :changes
|
||||||
|
get :autolink_candidates
|
||||||
|
|
||||||
scope :title do
|
scope :title do
|
||||||
get ':title/exists', action: :exists_by_title
|
get ':title/exists', action: :exists_by_title
|
||||||
|
|||||||
@@ -11,6 +11,29 @@ export const formatUIString = (text: string, language: string): string =>
|
|||||||
: text
|
: text
|
||||||
|
|
||||||
|
|
||||||
|
const splitAinuText = (text: string): string[] => {
|
||||||
|
const parts: string[] = []
|
||||||
|
let part = ''
|
||||||
|
let boundary = false
|
||||||
|
for (const character of text)
|
||||||
|
{
|
||||||
|
// Keep punctuation runs and closing brackets/quotes with the preceding word.
|
||||||
|
if (boundary && !/[\u3000、,,。.\p{Pe}\p{Pf}]/u.test (character))
|
||||||
|
{
|
||||||
|
parts.push (part)
|
||||||
|
part = ''
|
||||||
|
boundary = false
|
||||||
|
}
|
||||||
|
part += character
|
||||||
|
if (/[\u3000、,,。.]/u.test (character))
|
||||||
|
boundary = true
|
||||||
|
}
|
||||||
|
if (part !== '')
|
||||||
|
parts.push (part)
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const localiseUIChildren = (children: ReactNode, language: string): ReactNode => {
|
export const localiseUIChildren = (children: ReactNode, language: string): ReactNode => {
|
||||||
if (language !== 'ain' && language !== 'ain-Kana')
|
if (language !== 'ain' && language !== 'ain-Kana')
|
||||||
return children
|
return children
|
||||||
@@ -18,15 +41,28 @@ export const localiseUIChildren = (children: ReactNode, language: string): React
|
|||||||
return Children.map (children, child => {
|
return Children.map (children, child => {
|
||||||
if (typeof child === 'string')
|
if (typeof child === 'string')
|
||||||
{
|
{
|
||||||
return formatUIString (child, language).split (/(\u3000+)/).map ((part, index) =>
|
const parts = splitAinuText (formatUIString (child, language))
|
||||||
part.includes ('\u3000')
|
return parts.map ((part, index) => {
|
||||||
? <Fragment key={index}>{part}<wbr/></Fragment>
|
const spaces = part.match (/\u3000+$/u)?.[0] ?? ''
|
||||||
: <span key={index} className="ainu-ui-word">{part}</span>)
|
const word = part.slice (0, part.length - spaces.length)
|
||||||
|
return (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{word !== '' && <span className="ainu-ui-word">{word}</span>}
|
||||||
|
{spaces}
|
||||||
|
{index < parts.length - 1 && <wbr/>}
|
||||||
|
</Fragment>)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isValidElement<{ children?: ReactNode; lang?: string }> (child))
|
if (isValidElement<{ children?: ReactNode; lang?: string }> (child))
|
||||||
{
|
{
|
||||||
if (child.type === 'code' || child.type === 'pre'
|
// Only traverse inline formatting owned by the renderer. Components,
|
||||||
|
// links and code retain their own children and behaviour.
|
||||||
|
const inline = child.type === Fragment
|
||||||
|
|| (typeof child.type === 'string'
|
||||||
|
&& ['span', 'em', 'strong', 'del', 's', 'b', 'i', 'u',
|
||||||
|
'small', 'mark', 'sup', 'sub'].includes (child.type))
|
||||||
|
if (!inline
|
||||||
|| (child.props.lang != null && !(child.props.lang.startsWith ('ain'))))
|
|| (child.props.lang != null && !(child.props.lang.startsWith ('ain'))))
|
||||||
return child
|
return child
|
||||||
return cloneElement (child, { }, localiseUIChildren (child.props.children, language))
|
return cloneElement (child, { }, localiseUIChildren (child.props.children, language))
|
||||||
|
|||||||
@@ -27,66 +27,64 @@ const PostOriginalCreatedTimeField: FC<Props> = (
|
|||||||
<FormField label={label ('labels.originalCreatedAt')} messages={errors}>
|
<FormField label={label ('labels.originalCreatedAt')} messages={errors}>
|
||||||
{({ describedBy, invalid }) => (
|
{({ describedBy, invalid }) => (
|
||||||
<>
|
<>
|
||||||
<div className="my-1 flex">
|
<div className="my-1 flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<div className="w-80">
|
<DateTimeField
|
||||||
<DateTimeField
|
className="min-w-0 max-w-full"
|
||||||
className="mr-2"
|
disabled={disabled ?? false}
|
||||||
disabled={disabled ?? false}
|
aria-describedby={describedBy}
|
||||||
aria-describedby={describedBy}
|
aria-invalid={invalid}
|
||||||
aria-invalid={invalid}
|
invalid={invalid}
|
||||||
invalid={invalid}
|
value={originalCreatedFrom ?? undefined}
|
||||||
value={originalCreatedFrom ?? undefined}
|
onChange={setOriginalCreatedFrom}
|
||||||
onChange={setOriginalCreatedFrom}
|
onBlur={ev => {
|
||||||
onBlur={ev => {
|
const v = ev.target.value
|
||||||
const v = ev.target.value
|
if (!(v))
|
||||||
if (!(v))
|
return
|
||||||
return
|
|
||||||
|
|
||||||
const d = new Date (v)
|
const d = new Date (v)
|
||||||
if (d.getMinutes () === 0 && d.getHours () === 0)
|
if (d.getMinutes () === 0 && d.getHours () === 0)
|
||||||
d.setDate (d.getDate () + 1)
|
d.setDate (d.getDate () + 1)
|
||||||
else
|
else
|
||||||
d.setMinutes (d.getMinutes () + 1)
|
d.setMinutes (d.getMinutes () + 1)
|
||||||
setOriginalCreatedBefore (toMinutePrecisionIsoUtc (d.toISOString ()))
|
setOriginalCreatedBefore (toMinutePrecisionIsoUtc (d.toISOString ()))
|
||||||
}}/>
|
}}/>
|
||||||
|
<span className="ainu-ui-label shrink-0 whitespace-nowrap">
|
||||||
{label ('labels.since')}
|
{label ('labels.since')}
|
||||||
</div>
|
</span>
|
||||||
<div>
|
<Button
|
||||||
<Button
|
type="button"
|
||||||
type="button"
|
className="ainu-ui-label shrink-0 rounded bg-gray-600 text-white
|
||||||
className="bg-gray-600 text-white rounded"
|
dark:bg-gray-600 dark:text-white"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOriginalCreatedFrom (null)
|
setOriginalCreatedFrom (null)
|
||||||
}}>
|
}}>
|
||||||
{label ('actions.reset')}
|
{label ('actions.reset')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="my-1 flex">
|
<div className="my-1 flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<div className="w-80">
|
<DateTimeField
|
||||||
<DateTimeField
|
className="min-w-0 max-w-full"
|
||||||
className="mr-2"
|
disabled={disabled}
|
||||||
disabled={disabled}
|
aria-describedby={describedBy}
|
||||||
aria-describedby={describedBy}
|
aria-invalid={invalid}
|
||||||
aria-invalid={invalid}
|
invalid={invalid}
|
||||||
invalid={invalid}
|
value={originalCreatedBefore ?? undefined}
|
||||||
value={originalCreatedBefore ?? undefined}
|
onChange={setOriginalCreatedBefore}/>
|
||||||
onChange={setOriginalCreatedBefore}/>
|
<span className="ainu-ui-label shrink-0 whitespace-nowrap">
|
||||||
{label ('labels.before')}
|
{label ('labels.before')}
|
||||||
</div>
|
</span>
|
||||||
<div>
|
<Button
|
||||||
<Button
|
type="button"
|
||||||
type="button"
|
className="ainu-ui-label shrink-0 rounded bg-gray-600 text-white
|
||||||
className="bg-gray-600 text-white rounded"
|
dark:bg-gray-600 dark:text-white"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOriginalCreatedBefore (null)
|
setOriginalCreatedBefore (null)
|
||||||
}}>
|
}}>
|
||||||
{label ('actions.reset')}
|
{label ('actions.reset')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>)}
|
</>)}
|
||||||
</FormField>)
|
</FormField>)
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ import { useUITranslation } from '@/lib/useUITranslation'
|
|||||||
import PrefetchLink from '@/components/PrefetchLink'
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import { wikiKeys } from '@/lib/queryKeys'
|
import { wikiKeys } from '@/lib/queryKeys'
|
||||||
import remarkWikiAutoLink from '@/lib/remark-wiki-autolink'
|
import remarkWikiAutoLink from '@/lib/remark-wiki-autolink'
|
||||||
import { fetchWikiPages } from '@/lib/wiki'
|
import { fetchWikiAutolinkCandidates } from '@/lib/wiki'
|
||||||
|
import { ainuMDXComponents, LocalisedDocument } from '@/mdx-components'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { Components } from 'react-markdown'
|
import type { Components } from 'react-markdown'
|
||||||
|
|
||||||
type Props = { title: string
|
type Props = { title: string
|
||||||
body?: string }
|
body?: string
|
||||||
|
language?: string }
|
||||||
|
|
||||||
const mdComponents = { a: (({ href, children }) => (
|
const mdComponents = { a: (({ href, children }) => (
|
||||||
['/', '.'].some (e => href?.startsWith (e))
|
['/', '.'].some (e => href?.startsWith (e))
|
||||||
@@ -27,23 +29,32 @@ const mdComponents = { a: (({ href, children }) => (
|
|||||||
</a>))) } as const satisfies Components
|
</a>))) } as const satisfies Components
|
||||||
|
|
||||||
|
|
||||||
const WikiBody: FC<Props> = ({ title, body }) => {
|
const WikiBody: FC<Props> = ({ title, body, language = 'und' }) => {
|
||||||
const { message } = useUITranslation ('wiki')
|
const { message } = useUITranslation ('wiki')
|
||||||
const locale = useLocale ()
|
const locale = useLocale ()
|
||||||
|
const missingPage = message ('missingPage', {
|
||||||
|
url: `/wiki/new?title=${ encodeURIComponent (title) }` })
|
||||||
|
const contentLanguage = body ? language : missingPage.language
|
||||||
|
const matchLanguage = contentLanguage === 'ain-Kana' ? 'ain' : contentLanguage
|
||||||
const { data } = useQuery ({
|
const { data } = useQuery ({
|
||||||
enabled: Boolean (body),
|
enabled: Boolean (body),
|
||||||
queryKey: wikiKeys.index ({ }, locale),
|
queryKey: wikiKeys.autolinkCandidates (matchLanguage, locale),
|
||||||
queryFn: () => fetchWikiPages ({ }, locale) })
|
queryFn: () => fetchWikiAutolinkCandidates (matchLanguage, locale) })
|
||||||
const pageNames = (data ?? []).map (page => page.title).sort ((a, b) => b.length - a.length)
|
|
||||||
|
|
||||||
const remarkPlugins = useMemo (
|
const remarkPlugins = useMemo (
|
||||||
() => [() => remarkWikiAutoLink (pageNames), remarkGFM], [pageNames])
|
() => [() => remarkWikiAutoLink (data ?? [], matchLanguage), remarkGFM],
|
||||||
|
[data, matchLanguage])
|
||||||
|
const components = {
|
||||||
|
...((contentLanguage === 'ain' || contentLanguage === 'ain-Kana')
|
||||||
|
? ainuMDXComponents : { }),
|
||||||
|
...mdComponents }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown components={mdComponents} remarkPlugins={remarkPlugins}>
|
<LocalisedDocument language={contentLanguage}>
|
||||||
{body || message ('missingPage', {
|
<ReactMarkdown components={components} remarkPlugins={remarkPlugins}>
|
||||||
url: `/wiki/new?title=${ encodeURIComponent (title) }` }).text}
|
{body || missingPage.text}
|
||||||
</ReactMarkdown>)
|
</ReactMarkdown>
|
||||||
|
</LocalisedDocument>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WikiBody
|
export default WikiBody
|
||||||
|
|||||||
@@ -13,11 +13,36 @@
|
|||||||
|
|
||||||
.ainu-ui-word
|
.ainu-ui-word
|
||||||
{
|
{
|
||||||
|
display: inline-block;
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
word-break: keep-all !important;
|
word-break: keep-all !important;
|
||||||
overflow-wrap: normal !important;
|
overflow-wrap: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ainu-ui-label .ainu-ui-text
|
||||||
|
{
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ainu-ui-label wbr
|
||||||
|
{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.localised-document
|
||||||
|
{
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.localised-document a:lang(ain)
|
||||||
|
{
|
||||||
|
white-space: nowrap;
|
||||||
|
word-break: keep-all;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
}
|
||||||
|
|
||||||
@layer base
|
@layer base
|
||||||
{
|
{
|
||||||
:root
|
:root
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { localeFromPath, localeLanguage, unlocalisedPath } from '@/lib/locales'
|
|||||||
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
|
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
|
||||||
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||||
import { fetchNicoTags, fetchTagByName, fetchTag, fetchTagChanges, fetchTags } from '@/lib/tags'
|
import { fetchNicoTags, fetchTagByName, fetchTag, fetchTagChanges, fetchTags } from '@/lib/tags'
|
||||||
import { fetchWikiPage,
|
import { fetchWikiAutolinkCandidates,
|
||||||
fetchWikiPageByTitle,
|
fetchWikiPageByReference,
|
||||||
fetchWikiPages } from '@/lib/wiki'
|
fetchWikiPages } from '@/lib/wiki'
|
||||||
|
|
||||||
import type { LocaleCode } from '@/lib/locales'
|
import type { LocaleCode } from '@/lib/locales'
|
||||||
@@ -114,22 +114,16 @@ const prefetchWikiPageShow: Prefetcher = async (qc, url, locale) => {
|
|||||||
|
|
||||||
const wikiPage = await qc.fetchQuery ({
|
const wikiPage = await qc.fetchQuery ({
|
||||||
queryKey: wikiKeys.show (title, { version }, locale),
|
queryKey: wikiKeys.show (title, { version }, locale),
|
||||||
queryFn: () => fetchWikiPageByTitle (title, { version }, locale) })
|
queryFn: () => fetchWikiPageByReference (title, { version }, locale) })
|
||||||
|
|
||||||
if (wikiPage)
|
if (wikiPage?.body)
|
||||||
{
|
{
|
||||||
const effectiveId = String (wikiPage.id ?? '')
|
const language = wikiPage.languageCode
|
||||||
await qc.prefetchQuery ({
|
?? (wikiPage.resourceType === 'tag_description' ? localeLanguage (locale) : 'und')
|
||||||
queryKey: wikiKeys.show (effectiveId, { }, locale),
|
await qc.prefetchQuery ({
|
||||||
queryFn: () => fetchWikiPage (effectiveId, { }, locale ) })
|
queryKey: wikiKeys.autolinkCandidates (language, locale),
|
||||||
|
queryFn: () => fetchWikiAutolinkCandidates (language, locale) })
|
||||||
if (wikiPage.body)
|
}
|
||||||
{
|
|
||||||
await qc.prefetchQuery ({
|
|
||||||
queryKey: wikiKeys.index ({ }, locale),
|
|
||||||
queryFn: () => fetchWikiPages ({ }, locale) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const effectiveTitle = wikiPage?.title ?? title
|
const effectiveTitle = wikiPage?.title ?? title
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,8 @@ export const materialsKeys = {
|
|||||||
|
|
||||||
export const wikiKeys = {
|
export const wikiKeys = {
|
||||||
root: ['wiki'] as const,
|
root: ['wiki'] as const,
|
||||||
|
autolinkCandidates: (language: string, locale: LocaleCode) =>
|
||||||
|
['wiki', 'autolink', locale, language] as const,
|
||||||
index: (
|
index: (
|
||||||
p: { title?: string },
|
p: { title?: string },
|
||||||
locale: LocaleCode = 'ja',
|
locale: LocaleCode = 'ja',
|
||||||
|
|||||||
@@ -1,101 +1,128 @@
|
|||||||
import type { Content, Parent, Root, RootContent } from 'mdast'
|
import type { Content, Parent, Root, RootContent } from 'mdast'
|
||||||
|
|
||||||
|
import type { WikiAutolinkCandidate } from '@/types'
|
||||||
|
|
||||||
const escapeForRegExp = (s: string) => s.replace (/[.*+?^${}()|[\]\\]/g, '\\$&')
|
const escapeForRegExp = (s: string) => s.replace (/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
|
||||||
|
|
||||||
export default (pageNames: string[], basePath = '/wiki'): ((tree: Root) => void) => {
|
const wordCharacter = /[\p{L}\p{M}\p{N}_]/u
|
||||||
const names = [...pageNames].sort ((a, b) => b.length - a.length)
|
const excluded = ['link', 'linkReference', 'image', 'imageReference',
|
||||||
|
'code', 'inlineCode', 'html']
|
||||||
|
|
||||||
if (names.length === 0)
|
|
||||||
|
const edgeCharacter = (node: Content, first: boolean): string => {
|
||||||
|
if (node.type === 'text' || node.type === 'inlineCode')
|
||||||
|
{
|
||||||
|
const characters = Array.from (node.value)
|
||||||
|
return (first ? characters[0] : characters.at (-1)) ?? ''
|
||||||
|
}
|
||||||
|
if ('children' in node)
|
||||||
|
{
|
||||||
|
const children = first ? node.children : [...node.children].reverse ()
|
||||||
|
for (const child of children)
|
||||||
{
|
{
|
||||||
return () => {
|
const character = edgeCharacter (child, first)
|
||||||
;
|
if (character !== '')
|
||||||
}
|
return character
|
||||||
}
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return '\u3000'
|
||||||
|
}
|
||||||
|
|
||||||
const re = new RegExp (`(${ names.map (escapeForRegExp).join ('|') })`, 'g')
|
|
||||||
|
const remarkWikiAutoLink = (
|
||||||
|
candidates: WikiAutolinkCandidate[],
|
||||||
|
language: string,
|
||||||
|
): ((tree: Root) => void) => {
|
||||||
|
const targets = new Map<string, WikiAutolinkCandidate> ()
|
||||||
|
const ambiguous = new Set<string> ()
|
||||||
|
for (const candidate of candidates)
|
||||||
|
{
|
||||||
|
if (candidate.languageCode !== language || candidate.name.length === 0)
|
||||||
|
continue
|
||||||
|
const previous = targets.get (candidate.name)
|
||||||
|
if (previous != null
|
||||||
|
&& (previous.resourceType !== candidate.resourceType
|
||||||
|
|| previous.resourceId !== candidate.resourceId
|
||||||
|
|| previous.tagId !== candidate.tagId))
|
||||||
|
{
|
||||||
|
ambiguous.add (candidate.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targets.set (candidate.name, candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = [...targets.keys ()].sort ((a, b) => b.length - a.length)
|
||||||
|
if (names.length === 0)
|
||||||
|
return () => { }
|
||||||
|
|
||||||
|
const expression = new RegExp (names.map (escapeForRegExp).join ('|'), 'gu')
|
||||||
|
const ainu = language === 'ain'
|
||||||
|
|
||||||
return (tree: Root) => {
|
return (tree: Root) => {
|
||||||
const edits: { parent: Parent; index: number; parts: RootContent[] }[] = []
|
const edits: { parent: Parent; index: number; parts: RootContent[] }[] = []
|
||||||
|
const walk = (
|
||||||
const walk = (node: Content | Root, ancestors: Parent[]) => {
|
node: Content | Root,
|
||||||
if (!(node) || (typeof node !== 'object'))
|
parent?: Parent,
|
||||||
|
preceding = '',
|
||||||
|
following = '',
|
||||||
|
) => {
|
||||||
|
if (excluded.includes (node.type))
|
||||||
return
|
return
|
||||||
|
if (node.type === 'text' && parent != null)
|
||||||
if (!(ancestors.some (ancestor => ['link',
|
|
||||||
'linkReference',
|
|
||||||
'image',
|
|
||||||
'imageReference',
|
|
||||||
'code',
|
|
||||||
'inlineCode'].includes (ancestor?.type)))
|
|
||||||
&& (node.type === 'text'))
|
|
||||||
{
|
|
||||||
const value = node.value ?? ''
|
|
||||||
if (value)
|
|
||||||
{
|
|
||||||
re.lastIndex = 0
|
|
||||||
let m: RegExpExecArray | null
|
|
||||||
let last = 0
|
|
||||||
const parts: RootContent[] = []
|
|
||||||
|
|
||||||
while ((m = re.exec (value)) !== null)
|
|
||||||
{
|
|
||||||
const start = m.index
|
|
||||||
const end = start + m[0].length
|
|
||||||
|
|
||||||
if (start > last)
|
|
||||||
parts.push ({ type: 'text', value: value.slice (last, start) })
|
|
||||||
|
|
||||||
const name = m[1]
|
|
||||||
parts.push ({ type: 'link',
|
|
||||||
url: `${ basePath }/${ encodeURIComponent (name) }`,
|
|
||||||
title: null,
|
|
||||||
children: [{ type: 'text', value: name }],
|
|
||||||
data: { hProperties: { 'data-wiki': '1' } } })
|
|
||||||
last = end
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length)
|
|
||||||
{
|
|
||||||
if (last < value.length)
|
|
||||||
parts.push ({ type: 'text', value: value.slice (last) })
|
|
||||||
const parent = ancestors[ancestors.length - 1]
|
|
||||||
if (parent && Array.isArray (parent.children))
|
|
||||||
{
|
|
||||||
const index = parent.children.indexOf (node)
|
|
||||||
if (index >= 0)
|
|
||||||
edits.push ({ parent, index, parts })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maybeChidren = 'children' in node ? node.children : undefined
|
|
||||||
if (Array.isArray (maybeChidren))
|
|
||||||
{
|
|
||||||
const parent = node as Parent
|
|
||||||
|
|
||||||
for (let i = 0; i < maybeChidren.length; ++i)
|
|
||||||
{
|
|
||||||
const child: Content | undefined = maybeChidren[i]
|
|
||||||
if (!(child))
|
|
||||||
continue
|
|
||||||
walk (child, ancestors.concat (parent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
walk (tree, [])
|
|
||||||
|
|
||||||
for (let i = edits.length - 1; i >= 0; --i)
|
|
||||||
{
|
{
|
||||||
const { parent, index, parts } = edits[i]
|
const value = node.value
|
||||||
|
let last = 0
|
||||||
if (!(parent) || !(Array.isArray (parent.children)))
|
const parts: RootContent[] = []
|
||||||
continue
|
for (const match of value.matchAll (expression))
|
||||||
|
{
|
||||||
if (0 <= index && index < parent.children.length)
|
const start = match.index
|
||||||
parent.children.splice (index, 1, ...parts)
|
const end = start + match[0].length
|
||||||
|
const candidate = targets.get (match[0])
|
||||||
|
const before = Array.from (value.slice (0, start)).at (-1) ?? preceding
|
||||||
|
const after = Array.from (value.slice (end))[0] ?? following
|
||||||
|
if (candidate == null || ambiguous.has (match[0])
|
||||||
|
|| (ainu && (wordCharacter.test (before) || wordCharacter.test (after))))
|
||||||
|
continue
|
||||||
|
if (start > last)
|
||||||
|
parts.push ({ type: 'text', value: value.slice (last, start) })
|
||||||
|
parts.push ({
|
||||||
|
type: 'link',
|
||||||
|
url: `/${candidate.localeCode}/wiki/${encodeURIComponent (candidate.resourceId)}`,
|
||||||
|
title: null,
|
||||||
|
children: [{ type: 'text', value: match[0] }],
|
||||||
|
data: { hProperties: { 'data-wiki': '1' } } })
|
||||||
|
last = end
|
||||||
|
}
|
||||||
|
if (parts.length > 0)
|
||||||
|
{
|
||||||
|
if (last < value.length)
|
||||||
|
parts.push ({ type: 'text', value: value.slice (last) })
|
||||||
|
edits.push ({ parent, index: parent.children.indexOf (node), parts })
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if ('children' in node)
|
||||||
|
{
|
||||||
|
const inline = ['paragraph', 'heading', 'emphasis', 'strong',
|
||||||
|
'delete', 'tableCell'].includes (node.type)
|
||||||
|
for (let index = 0; index < node.children.length; ++index)
|
||||||
|
{
|
||||||
|
const before = index > 0
|
||||||
|
? edgeCharacter (node.children[index - 1], false) : preceding
|
||||||
|
const after = index + 1 < node.children.length
|
||||||
|
? edgeCharacter (node.children[index + 1], true) : following
|
||||||
|
walk (node.children[index], node as Parent,
|
||||||
|
inline ? before : '', inline ? after : '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk (tree)
|
||||||
|
for (const { parent, index, parts } of edits.reverse ())
|
||||||
|
parent.children.splice (index, 1, ...parts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default remarkWikiAutoLink
|
||||||
|
|||||||
+30
-2
@@ -3,7 +3,14 @@ import { isAxiosError } from 'axios'
|
|||||||
import { apiGet } from '@/lib/api'
|
import { apiGet } from '@/lib/api'
|
||||||
|
|
||||||
import type { LocaleCode } from '@/lib/locales'
|
import type { LocaleCode } from '@/lib/locales'
|
||||||
import type { WikiPage, WikiPageChange } from '@/types'
|
import type { WikiAutolinkCandidate, WikiPage, WikiPageChange } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
|
export const fetchWikiAutolinkCandidates = (
|
||||||
|
language: string,
|
||||||
|
locale: LocaleCode,
|
||||||
|
): Promise<WikiAutolinkCandidate[]> =>
|
||||||
|
apiGet ('/wiki/autolink_candidates', { locale, params: { language } })
|
||||||
|
|
||||||
|
|
||||||
export const fetchWikiChanges = (
|
export const fetchWikiChanges = (
|
||||||
@@ -24,7 +31,8 @@ export const fetchWikiPage = async (
|
|||||||
{ version }: { version?: string },
|
{ version }: { version?: string },
|
||||||
locale: LocaleCode = 'ja',
|
locale: LocaleCode = 'ja',
|
||||||
): Promise<WikiPage> =>
|
): Promise<WikiPage> =>
|
||||||
await apiGet (`/wiki/${ id }`, { locale, params: version ? { version } : { } })
|
await apiGet (`/wiki/${ encodeURIComponent (id) }`, {
|
||||||
|
locale, params: version ? { version } : { } })
|
||||||
|
|
||||||
|
|
||||||
export const fetchWikiPageByTitle = async (
|
export const fetchWikiPageByTitle = async (
|
||||||
@@ -43,3 +51,23 @@ export const fetchWikiPageByTitle = async (
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const fetchWikiPageByReference = async (
|
||||||
|
reference: string,
|
||||||
|
params: { version?: string },
|
||||||
|
locale: LocaleCode,
|
||||||
|
): Promise<WikiPage | null> => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return /^\d+$/.test (reference)
|
||||||
|
? await fetchWikiPage (reference, params, locale)
|
||||||
|
: await fetchWikiPageByTitle (reference, params, locale)
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
if (isAxiosError (error) && error.response?.status === 404)
|
||||||
|
return null
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"list": {
|
"list": {
|
||||||
"tagline": "{{brand}} | ぼざろクリーチャーシリーズ ウコテㇷ゚ オピッタ",
|
"tagline": "{{brand}}",
|
||||||
"empty": "ミンタㇻ タ ネン カ イサㇺ(笑)",
|
"empty": "ミンタㇻ タ ネン カ イサㇺ",
|
||||||
"viewWiki": "Wiki ヌカㇻ",
|
"viewWiki": "Wiki ヌカㇻ",
|
||||||
"and": " AND ",
|
"and": " AND ",
|
||||||
"or": " OR "
|
"or": " OR "
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"parentAndSiblings": "タン ミンタㇻ コㇿ サパ ミンタㇻ シネ ペ ネワ ウコシㇼキ ミンタㇻ {{count}} ペ アン。",
|
"parentAndSiblings": "タン ミンタㇻ コㇿ サパ ミンタㇻ シネ ペ ネワ ウコシㇼキ ミンタㇻ {{count}} ペ アン。",
|
||||||
"viewed": "アヌカㇻ オケレ",
|
"viewed": "アヌカㇻ オケレ",
|
||||||
"unviewed": "ソモ アヌカㇻ",
|
"unviewed": "ソモ アヌカㇻ",
|
||||||
"emptyRelated": "ネン カ イサㇺ(笑)"
|
"emptyRelated": "ネン カ イサㇺ"
|
||||||
},
|
},
|
||||||
"embed": {
|
"embed": {
|
||||||
"openExternal": "ソイタㇰ ソㇱ アㇱマカ",
|
"openExternal": "ソイタㇰ ソㇱ アㇱマカ",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createElement, useMemo } from 'react'
|
import { createContext, createElement, useContext, useMemo } from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import PrefetchLink from '@/components/PrefetchLink'
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
@@ -6,7 +6,24 @@ import { localiseUIChildren } from '@/components/LocalisedText'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import type { MDXComponents } from 'mdx/types'
|
import type { MDXComponents } from 'mdx/types'
|
||||||
import type { ComponentProps, HTMLAttributes } from 'react'
|
import type { ComponentProps, HTMLAttributes, ReactNode } from 'react'
|
||||||
|
|
||||||
|
|
||||||
|
const DocumentLanguageContext = createContext ('ja')
|
||||||
|
|
||||||
|
|
||||||
|
export const LocalisedDocument = (
|
||||||
|
{ language, children, className }: {
|
||||||
|
language: string
|
||||||
|
children: ReactNode
|
||||||
|
className?: string },
|
||||||
|
) => (
|
||||||
|
<DocumentLanguageContext.Provider value={language}>
|
||||||
|
<div lang={language === 'ain' ? 'ain-Kana' : language}
|
||||||
|
className={cn ('localised-document', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</DocumentLanguageContext.Provider>)
|
||||||
|
|
||||||
|
|
||||||
const MDXLink = (
|
const MDXLink = (
|
||||||
@@ -51,20 +68,40 @@ const MDXLink = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const useMDXComponents = (): MDXComponents => ({ a: MDXLink })
|
export const useMDXComponents = (): MDXComponents => {
|
||||||
|
const language = useContext (DocumentLanguageContext)
|
||||||
|
return {
|
||||||
|
a: MDXLink,
|
||||||
|
...((language === 'ain' || language === 'ain-Kana') ? ainuMDXComponents : { }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const ainuElement = (element: string) => {
|
const ainuElement = (element: string) => {
|
||||||
const AinuElement = ({ children, className, ...props }: HTMLAttributes<HTMLElement>) =>
|
const AinuElement = (
|
||||||
createElement (element,
|
{ children, className, ...props }: HTMLAttributes<HTMLElement> & {
|
||||||
{ ...props, lang: 'ain-Kana', className: cn ('ainu-ui-text', className) },
|
node?: unknown },
|
||||||
localiseUIChildren (children, 'ain-Kana'))
|
) => {
|
||||||
|
const documentLanguage = useContext (DocumentLanguageContext)
|
||||||
|
const language = props.lang ?? documentLanguage
|
||||||
|
// ReactMarkdown supplies its syntax node; it is not a DOM attribute.
|
||||||
|
delete props.node
|
||||||
|
return (
|
||||||
|
<DocumentLanguageContext.Provider value={language}>
|
||||||
|
{createElement (element,
|
||||||
|
{ ...props,
|
||||||
|
lang: language === 'ain' ? 'ain-Kana' : language,
|
||||||
|
className: cn (
|
||||||
|
(language === 'ain' || language === 'ain-Kana') && 'ainu-ui-text',
|
||||||
|
className) },
|
||||||
|
localiseUIChildren (children, language))}
|
||||||
|
</DocumentLanguageContext.Provider>)
|
||||||
|
}
|
||||||
return AinuElement
|
return AinuElement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Opt in only for authored Ainu UI documents, never user-supplied Markdown.
|
// Opt in using the document's language, independently of the URL locale.
|
||||||
export const ainuMDXComponents: MDXComponents = {
|
export const ainuMDXComponents = {
|
||||||
p: ainuElement ('p'),
|
p: ainuElement ('p'),
|
||||||
h1: ainuElement ('h1'),
|
h1: ainuElement ('h1'),
|
||||||
h2: ainuElement ('h2'),
|
h2: ainuElement ('h2'),
|
||||||
@@ -76,4 +113,7 @@ export const ainuMDXComponents: MDXComponents = {
|
|||||||
dt: ainuElement ('dt'),
|
dt: ainuElement ('dt'),
|
||||||
dd: ainuElement ('dd'),
|
dd: ainuElement ('dd'),
|
||||||
th: ainuElement ('th'),
|
th: ainuElement ('th'),
|
||||||
td: ainuElement ('td') }
|
td: ainuElement ('td'),
|
||||||
|
em: ainuElement ('em'),
|
||||||
|
strong: ainuElement ('strong'),
|
||||||
|
del: ainuElement ('del') }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const lastUpdatedAt = localisedDateString ('2026-04-12', 'ain-Kana', 'hou
|
|||||||
<article lang="ain-Kana" className="prose dark:prose-invert mx-auto p-4">
|
<article lang="ain-Kana" className="prose dark:prose-invert mx-auto p-4">
|
||||||
# アエイワンケ イタㇰ
|
# アエイワンケ イタㇰ
|
||||||
|
|
||||||
イオッタ アシㇼ アヌイェ ト: {lastUpdatedAt}
|
イオッタ アシㇼ アヌイェ ト: {lastUpdatedAt}
|
||||||
|
|
||||||
タン アエイワンケ イタㇰ(タプ ワ 「タン イタㇰ」 セコㇿ アニェ) アナㇰネ、ポチ チャ ロㇰ シクヌㇷ゚ イキㇼ チセ(タプ ワ 「タン チヘコテㇷ゚」 セコㇿ アニェ) アエイワンケ ヒ オㇿ アヌイェ イタㇰ ネ。 タン チヘコテㇷ゚ エイワンケ クㇽ アナㇰネ、エイワンケ ヒ タ タン イタㇰ エセ ルウェ ネ セコㇿ インカルㇱクㇽ ラム。
|
タン アエイワンケ イタㇰ(タプ ワ 「タン イタㇰ」 セコㇿ アニェ) アナㇰネ、ポチ チャ ロㇰ シクヌㇷ゚ イキㇼ チセ(タプ ワ 「タン チヘコテㇷ゚」 セコㇿ アニェ) アエイワンケ ヒ オㇿ アヌイェ イタㇰ ネ。 タン チヘコテㇷ゚ エイワンケ クㇽ アナㇰネ、エイワンケ ヒ タ タン イタㇰ エセ ルウェ ネ セコㇿ インカルㇱクㇽ ラム。
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import PageHead from '@/components/common/PageHead'
|
import PageHead from '@/components/common/PageHead'
|
||||||
import { useLocale } from '@/lib/useLocale'
|
import { useLocale } from '@/lib/useLocale'
|
||||||
import { useUITranslation } from '@/lib/useUITranslation'
|
import { useUITranslation } from '@/lib/useUITranslation'
|
||||||
import { ainuMDXComponents } from '@/mdx-components'
|
import { LocalisedDocument } from '@/mdx-components'
|
||||||
import AinuTerms from '@/pages/TOSPage/ain-Kana.mdx'
|
|
||||||
import NipponeseTerms from '@/pages/TOSPage/ja.mdx'
|
import type { ComponentType } from 'react'
|
||||||
|
|
||||||
const terms = import.meta.glob<ComponentType> ('./*.mdx', { eager: true, import: 'default' })
|
const terms = import.meta.glob<ComponentType> ('./*.mdx', { eager: true, import: 'default' })
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ const TOSPage = () => {
|
|||||||
const locale = useLocale ()
|
const locale = useLocale ()
|
||||||
const { message } = useUITranslation ('navigation')
|
const { message } = useUITranslation ('navigation')
|
||||||
const Terms = terms[`./${ locale }.mdx`] ?? terms['./ja.mdx']
|
const Terms = terms[`./${ locale }.mdx`] ?? terms['./ja.mdx']
|
||||||
|
const language = terms[`./${ locale }.mdx`] != null ? locale : 'ja'
|
||||||
|
|
||||||
if (!terms)
|
if (!terms)
|
||||||
throw new Error ('TOSPage: ja.mdx not found')
|
throw new Error ('TOSPage: ja.mdx not found')
|
||||||
@@ -19,7 +20,9 @@ const TOSPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={message ('terms')}/>
|
<PageHead title={message ('terms')}/>
|
||||||
<Terms/>
|
<LocalisedDocument language={language} className="flex-1">
|
||||||
|
<Terms/>
|
||||||
|
</LocalisedDocument>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useLocale } from '@/lib/useLocale'
|
|||||||
import { useUITranslation } from '@/lib/useUITranslation'
|
import { useUITranslation } from '@/lib/useUITranslation'
|
||||||
import { dateString } from '@/lib/utils'
|
import { dateString } from '@/lib/utils'
|
||||||
import { helpKeys } from '@/lib/queryKeys'
|
import { helpKeys } from '@/lib/queryKeys'
|
||||||
|
import { LocalisedDocument } from '@/mdx-components'
|
||||||
import NotFound from '@/pages/NotFound'
|
import NotFound from '@/pages/NotFound'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
@@ -53,8 +54,8 @@ const HelpDetailPage: FC = () => {
|
|||||||
|
|
||||||
const { data: current, isSuccess, isError } = useQuery ({
|
const { data: current, isSuccess, isError } = useQuery ({
|
||||||
enabled: edition != null && editionInfo != null && editions.includes (edition),
|
enabled: edition != null && editionInfo != null && editions.includes (edition),
|
||||||
queryKey: helpKeys.edition (identifier, requestedLanguage, edition ?? ''),
|
queryKey: helpKeys.edition (identifier, language ?? requestedLanguage, edition ?? ''),
|
||||||
queryFn: () => getHelpEditionContent (identifier, requestedLanguage, edition ?? ''),
|
queryFn: () => getHelpEditionContent (identifier, language ?? requestedLanguage, edition ?? ''),
|
||||||
staleTime: Infinity })
|
staleTime: Infinity })
|
||||||
|
|
||||||
if (!articleLoading
|
if (!articleLoading
|
||||||
@@ -71,7 +72,7 @@ const HelpDetailPage: FC = () => {
|
|||||||
const Content = current?.content
|
const Content = current?.content
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainArea>
|
<MainArea className="min-w-0">
|
||||||
<PageHead title={message ('articleTitle', { title: current?.title || identifier })}/>
|
<PageHead title={message ('articleTitle', { title: current?.title || identifier })}/>
|
||||||
|
|
||||||
{requestedEdition && (
|
{requestedEdition && (
|
||||||
@@ -99,8 +100,14 @@ const HelpDetailPage: FC = () => {
|
|||||||
: label ('shared:actions.latest')}
|
: label ('shared:actions.latest')}
|
||||||
</div>)}
|
</div>)}
|
||||||
|
|
||||||
<article lang={requestedLanguage} className="prose dark:prose-invert mx-auto p-4">
|
<article lang={language ?? requestedLanguage}
|
||||||
{Content ? <Content/> : <div>{label ('common:loading')}</div>}
|
className="prose dark:prose-invert mx-auto min-w-0 p-4">
|
||||||
|
{Content
|
||||||
|
? (
|
||||||
|
<LocalisedDocument language={language ?? requestedLanguage}>
|
||||||
|
<Content/>
|
||||||
|
</LocalisedDocument>)
|
||||||
|
: <div>{label ('common:loading')}</div>}
|
||||||
</article>
|
</article>
|
||||||
</MainArea>)
|
</MainArea>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
updateMaterial,
|
updateMaterial,
|
||||||
} from '@/lib/materials'
|
} from '@/lib/materials'
|
||||||
import { materialsKeys } from '@/lib/queryKeys'
|
import { materialsKeys } from '@/lib/queryKeys'
|
||||||
|
import { localeLanguage } from '@/lib/locales'
|
||||||
import { inputClass } from '@/lib/utils'
|
import { inputClass } from '@/lib/utils'
|
||||||
import { useValidationErrors } from '@/lib/useValidationErrors'
|
import { useValidationErrors } from '@/lib/useValidationErrors'
|
||||||
import { useUITranslation } from '@/lib/useUITranslation'
|
import { useUITranslation } from '@/lib/useUITranslation'
|
||||||
@@ -132,6 +133,7 @@ const MaterialDetailPage: FC = () => {
|
|||||||
<Tab name={label ('navigation:wiki')}>
|
<Tab name={label ('navigation:wiki')}>
|
||||||
<WikiBody
|
<WikiBody
|
||||||
title={material.tag.name}
|
title={material.tag.name}
|
||||||
|
language={localeLanguage (locale)}
|
||||||
body={material.wikiPageBody ?? undefined}/>
|
body={material.wikiPageBody ?? undefined}/>
|
||||||
</Tab>)}
|
</Tab>)}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import TabGroup, { Tab } from '@/components/common/TabGroup'
|
|||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import PageHead from '@/components/common/PageHead'
|
import PageHead from '@/components/common/PageHead'
|
||||||
import { fetchPosts } from '@/lib/posts'
|
import { fetchPosts } from '@/lib/posts'
|
||||||
|
import { localeLanguage } from '@/lib/locales'
|
||||||
import { postsKeys } from '@/lib/queryKeys'
|
import { postsKeys } from '@/lib/queryKeys'
|
||||||
import {
|
import {
|
||||||
DEFAULT_POST_LIST_LIMIT,
|
DEFAULT_POST_LIST_LIMIT,
|
||||||
@@ -130,8 +131,8 @@ const PostListPage: FC = () => {
|
|||||||
ref={containerRef}>
|
ref={containerRef}>
|
||||||
<PageHead
|
<PageHead
|
||||||
title={tags.length
|
title={tags.length
|
||||||
? tags.join (message (anyFlg ? 'list.or' : 'list.and').text)
|
? tags.join (message (anyFlg ? 'list.or' : 'list.and').text)
|
||||||
: message ('list.tagline', { brand: siteTitle.text })}
|
: message ('list.tagline', { brand: siteTitle.text })}
|
||||||
includeSiteName={tags.length > 0}/>
|
includeSiteName={tags.length > 0}/>
|
||||||
|
|
||||||
<TagSidebar posts={posts.slice (0, 20)} onClick={() => {
|
<TagSidebar posts={posts.slice (0, 20)} onClick={() => {
|
||||||
@@ -173,7 +174,9 @@ const PostListPage: FC = () => {
|
|||||||
</Tab>
|
</Tab>
|
||||||
{tags.length === 1 && (
|
{tags.length === 1 && (
|
||||||
<Tab name={label ('navigation:wiki')}>
|
<Tab name={label ('navigation:wiki')}>
|
||||||
<WikiBody title={tags[0]} body={wikiPage?.body}/>
|
<WikiBody title={tags[0]} body={wikiPage?.body}
|
||||||
|
language={wikiPage?.languageCode ?? (wikiPage?.resourceType === 'tag_description'
|
||||||
|
? localeLanguage (locale) : 'und')}/>
|
||||||
<div className="my-2">
|
<div className="my-2">
|
||||||
<PrefetchLink to={`/wiki/${ encodeURIComponent (tags[0]) }`}>
|
<PrefetchLink to={`/wiki/${ encodeURIComponent (tags[0]) }`}>
|
||||||
{label ('list.viewWiki')}
|
{label ('list.viewWiki')}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import PageHead from '@/components/common/PageHead'
|
|||||||
import { toast } from '@/components/ui/use-toast'
|
import { toast } from '@/components/ui/use-toast'
|
||||||
import { CATEGORIES } from '@/consts'
|
import { CATEGORIES } from '@/consts'
|
||||||
import { apiPut } from '@/lib/api'
|
import { apiPut } from '@/lib/api'
|
||||||
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
|
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||||
import { fetchTag } from '@/lib/tags'
|
import { fetchTag } from '@/lib/tags'
|
||||||
import { cn, inputClass } from '@/lib/utils'
|
import { cn, inputClass } from '@/lib/utils'
|
||||||
import { useValidationErrors } from '@/lib/useValidationErrors'
|
import { useValidationErrors } from '@/lib/useValidationErrors'
|
||||||
@@ -75,6 +75,7 @@ const TagDetailPage: FC = () => {
|
|||||||
|
|
||||||
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
||||||
qc.invalidateQueries ({ queryKey: tagsKeys.root })
|
qc.invalidateQueries ({ queryKey: tagsKeys.root })
|
||||||
|
qc.invalidateQueries ({ queryKey: wikiKeys.root })
|
||||||
toast ({ description: label ('shared:messages.updated') })
|
toast ({ description: label ('shared:messages.updated') })
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ import MainArea from '@/components/layout/MainArea'
|
|||||||
import PageHead from '@/components/common/PageHead'
|
import PageHead from '@/components/common/PageHead'
|
||||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
||||||
import { getHelpTitleIndex } from '@/lib/help'
|
import { getHelpTitleIndex } from '@/lib/help'
|
||||||
|
import { localeLanguage } from '@/lib/locales'
|
||||||
import { fetchPosts } from '@/lib/posts'
|
import { fetchPosts } from '@/lib/posts'
|
||||||
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||||
import { fetchTagByName } from '@/lib/tags'
|
import { fetchTagByName } from '@/lib/tags'
|
||||||
import { fetchWikiPage, fetchWikiPageByTitle } from '@/lib/wiki'
|
import { fetchWikiPageByReference } from '@/lib/wiki'
|
||||||
import NotFound from '@/pages/NotFound'
|
import NotFound from '@/pages/NotFound'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
@@ -29,6 +30,7 @@ const WikiDetailPage: FC = () => {
|
|||||||
const locale = useLocale ()
|
const locale = useLocale ()
|
||||||
const params = useParams ()
|
const params = useParams ()
|
||||||
const title = params.title ?? ''
|
const title = params.title ?? ''
|
||||||
|
const numericReference = /^\d+$/.test (title)
|
||||||
|
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
const navigate = useLocaleNavigate ()
|
const navigate = useLocaleNavigate ()
|
||||||
@@ -39,14 +41,14 @@ const WikiDetailPage: FC = () => {
|
|||||||
const version = query.get ('version') || undefined
|
const version = query.get ('version') || undefined
|
||||||
|
|
||||||
const { data: wikiPage, isLoading: loading, isSuccess, isFetching, error } = useQuery ({
|
const { data: wikiPage, isLoading: loading, isSuccess, isFetching, error } = useQuery ({
|
||||||
enabled: Boolean (title) && !(/^\d+$/.test (title)),
|
enabled: Boolean (title),
|
||||||
queryKey: wikiKeys.show (title, { version }, locale),
|
queryKey: wikiKeys.show (title, { version }, locale),
|
||||||
queryFn: () => fetchWikiPageByTitle (title, { version }, locale) })
|
queryFn: () => fetchWikiPageByReference (title, { version }, locale) })
|
||||||
|
|
||||||
const missingWiki = isSuccess && !(isFetching) && wikiPage == null
|
const missingWiki = isSuccess && !(isFetching) && wikiPage == null
|
||||||
const { data: titleIndex, isPending: lookupPending,
|
const { data: titleIndex, isPending: lookupPending,
|
||||||
isFetching: lookupFetching, error: lookupError } = useQuery ({
|
isFetching: lookupFetching, error: lookupError } = useQuery ({
|
||||||
enabled: missingWiki,
|
enabled: missingWiki && !numericReference,
|
||||||
queryKey: helpKeys.titles ('ja'),
|
queryKey: helpKeys.titles ('ja'),
|
||||||
queryFn: () => getHelpTitleIndex ('ja'),
|
queryFn: () => getHelpTitleIndex ('ja'),
|
||||||
staleTime: Infinity })
|
staleTime: Infinity })
|
||||||
@@ -56,16 +58,17 @@ const WikiDetailPage: FC = () => {
|
|||||||
? titleIndex[lookupTitle] : []), [titleIndex, lookupTitle])
|
? titleIndex[lookupTitle] : []), [titleIndex, lookupTitle])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
if (missingWiki && !(lookupFetching) && lookupError == null && identifiers.length === 1)
|
if (missingWiki && !numericReference
|
||||||
|
&& !(lookupFetching) && lookupError == null && identifiers.length === 1)
|
||||||
navigate (`/help/${ encodeURIComponent (identifiers[0]) }`, { replace: true })
|
navigate (`/help/${ encodeURIComponent (identifiers[0]) }`, { replace: true })
|
||||||
}, [missingWiki, lookupFetching, lookupError, identifiers, navigate])
|
}, [missingWiki, numericReference, lookupFetching, lookupError, identifiers, navigate])
|
||||||
|
|
||||||
const effectiveTitle = wikiPage?.title ?? title
|
const effectiveTitle = wikiPage?.title ?? title
|
||||||
const deprecated = wikiPage?.deprecatedAt != null
|
const deprecated = wikiPage?.deprecatedAt != null
|
||||||
|
|
||||||
const { data: tag, isPending: tagPending,
|
const { data: tag, isPending: tagPending,
|
||||||
isFetching: tagFetching, error: tagError } = useQuery ({
|
isFetching: tagFetching, error: tagError } = useQuery ({
|
||||||
enabled: Boolean (effectiveTitle),
|
enabled: Boolean (effectiveTitle) && (!numericReference || wikiPage != null),
|
||||||
queryKey: tagsKeys.show (effectiveTitle, locale),
|
queryKey: tagsKeys.show (effectiveTitle, locale),
|
||||||
queryFn: () => fetchTagByName (effectiveTitle, locale) })
|
queryFn: () => fetchTagByName (effectiveTitle, locale) })
|
||||||
|
|
||||||
@@ -75,7 +78,8 @@ const WikiDetailPage: FC = () => {
|
|||||||
createdFrom: '', createdTo: '', updatedFrom: '', updatedTo: '',
|
createdFrom: '', createdTo: '', updatedFrom: '', updatedTo: '',
|
||||||
order: 'original_created_at:desc' } as const
|
order: 'original_created_at:desc' } as const
|
||||||
const { data } = useQuery ({
|
const { data } = useQuery ({
|
||||||
enabled: Boolean (effectiveTitle) && !(version),
|
enabled: Boolean (effectiveTitle) && !(version)
|
||||||
|
&& (!numericReference || wikiPage != null),
|
||||||
queryKey: postsKeys.index (keys, locale),
|
queryKey: postsKeys.index (keys, locale),
|
||||||
queryFn: () => fetchPosts (keys, locale) })
|
queryFn: () => fetchPosts (keys, locale) })
|
||||||
const posts = data?.posts || []
|
const posts = data?.posts || []
|
||||||
@@ -86,39 +90,22 @@ const WikiDetailPage: FC = () => {
|
|||||||
|
|
||||||
WikiIdBus.set (wikiPage.id)
|
WikiIdBus.set (wikiPage.id)
|
||||||
|
|
||||||
if (wikiPage.title !== title)
|
if (!numericReference && wikiPage.title !== title)
|
||||||
navigate (`/wiki/${ encodeURIComponent(wikiPage.title) }`, { replace: true })
|
{
|
||||||
|
navigate ({ pathname: `/wiki/${ encodeURIComponent (wikiPage.title) }`,
|
||||||
|
search: location.search, hash: location.hash }, { replace: true })
|
||||||
|
}
|
||||||
|
|
||||||
return () => WikiIdBus.set (null)
|
return () => WikiIdBus.set (null)
|
||||||
}, [wikiPage, title, navigate])
|
}, [wikiPage, title, numericReference, navigate, location.search, location.hash])
|
||||||
|
|
||||||
useEffect (() => {
|
|
||||||
if (!(/^\d+$/.test (title)))
|
|
||||||
return
|
|
||||||
|
|
||||||
let active = true
|
|
||||||
void (async () => {
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const data = await fetchWikiPage (title, { }, locale)
|
|
||||||
if (active)
|
|
||||||
navigate (`/wiki/${ encodeURIComponent (data.title) }`, { replace: true })
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
;
|
|
||||||
}
|
|
||||||
}) ()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
active = false
|
|
||||||
}
|
|
||||||
}, [title, navigate, locale])
|
|
||||||
|
|
||||||
const displayError = error ?? (missingWiki ? lookupError ?? tagError : null)
|
const displayError = error ?? (missingWiki ? lookupError ?? tagError : null)
|
||||||
if (displayError != null)
|
if (displayError != null)
|
||||||
return <MainArea><p role="alert">{displayError.message}</p></MainArea>
|
return <MainArea><p role="alert">{displayError.message}</p></MainArea>
|
||||||
|
|
||||||
|
if (missingWiki && numericReference)
|
||||||
|
return <NotFound/>
|
||||||
|
|
||||||
const waiting = loading || (isFetching && wikiPage == null)
|
const waiting = loading || (isFetching && wikiPage == null)
|
||||||
|| (missingWiki && (lookupPending || lookupFetching || tagPending
|
|| (missingWiki && (lookupPending || lookupFetching || tagPending
|
||||||
|| (tagFetching && tag == null) || identifiers.length === 1))
|
|| (tagFetching && tag == null) || identifiers.length === 1))
|
||||||
@@ -161,7 +148,11 @@ const WikiDetailPage: FC = () => {
|
|||||||
{...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/>
|
{...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/>
|
||||||
{deprecated && label ('labels.deprecatedSuffix')}
|
{deprecated && label ('labels.deprecatedSuffix')}
|
||||||
</h1>
|
</h1>
|
||||||
{waiting ? label ('common:loading') : <WikiBody title={title} body={wikiPage?.body}/>}
|
{waiting
|
||||||
|
? label ('common:loading')
|
||||||
|
: <WikiBody title={title} body={wikiPage?.body}
|
||||||
|
language={wikiPage?.languageCode ?? (wikiPage?.resourceType === 'tag_description'
|
||||||
|
? localeLanguage (locale) : 'und')}/>}
|
||||||
|
|
||||||
{(!(version) && posts.length > 0) && (
|
{(!(version) && posts.length > 0) && (
|
||||||
<div className="not-prose">
|
<div className="not-prose">
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { CATEGORIES,
|
|||||||
|
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import type { LocaleCode } from '@/lib/locales'
|
||||||
|
|
||||||
export type Category = typeof CATEGORIES[number]
|
export type Category = typeof CATEGORIES[number]
|
||||||
|
|
||||||
export type Deerjikist = { platform: Platform; code: string }
|
export type Deerjikist = { platform: Platform; code: string }
|
||||||
@@ -459,6 +461,8 @@ export type ViewFlagBehavior = typeof ViewFlagBehavior[keyof typeof ViewFlagBeha
|
|||||||
export type WikiPage = {
|
export type WikiPage = {
|
||||||
id: number
|
id: number
|
||||||
resourceType: 'tag_description' | 'tag_group'
|
resourceType: 'tag_description' | 'tag_group'
|
||||||
|
tagId?: number
|
||||||
|
languageCode?: string
|
||||||
title: string
|
title: string
|
||||||
deprecatedAt: string | null
|
deprecatedAt: string | null
|
||||||
createdUserId: number
|
createdUserId: number
|
||||||
@@ -470,6 +474,15 @@ export type WikiPage = {
|
|||||||
pred: number | null
|
pred: number | null
|
||||||
succ: number | null }
|
succ: number | null }
|
||||||
|
|
||||||
|
export type WikiAutolinkCandidate = {
|
||||||
|
resourceType: 'tag_description' | 'tag_group'
|
||||||
|
resourceId: number
|
||||||
|
tagId: number | null
|
||||||
|
tagDescriptionId: number | null
|
||||||
|
name: string
|
||||||
|
languageCode: string
|
||||||
|
localeCode: LocaleCode }
|
||||||
|
|
||||||
export type WikiPageChange = {
|
export type WikiPageChange = {
|
||||||
revisionId: number
|
revisionId: number
|
||||||
pred: number | null
|
pred: number | null
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする