このコミットが含まれているのは:
@@ -11,6 +11,29 @@ export const formatUIString = (text: string, language: string): string =>
|
||||
: 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 => {
|
||||
if (language !== 'ain' && language !== 'ain-Kana')
|
||||
return children
|
||||
@@ -18,15 +41,28 @@ export const localiseUIChildren = (children: ReactNode, language: string): React
|
||||
return Children.map (children, child => {
|
||||
if (typeof child === 'string')
|
||||
{
|
||||
return formatUIString (child, language).split (/(\u3000+)/).map ((part, index) =>
|
||||
part.includes ('\u3000')
|
||||
? <Fragment key={index}>{part}<wbr/></Fragment>
|
||||
: <span key={index} className="ainu-ui-word">{part}</span>)
|
||||
const parts = splitAinuText (formatUIString (child, language))
|
||||
return parts.map ((part, index) => {
|
||||
const spaces = part.match (/\u3000+$/u)?.[0] ?? ''
|
||||
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 (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'))))
|
||||
return child
|
||||
return cloneElement (child, { }, localiseUIChildren (child.props.children, language))
|
||||
|
||||
@@ -27,66 +27,64 @@ const PostOriginalCreatedTimeField: FC<Props> = (
|
||||
<FormField label={label ('labels.originalCreatedAt')} messages={errors}>
|
||||
{({ describedBy, invalid }) => (
|
||||
<>
|
||||
<div className="my-1 flex">
|
||||
<div className="w-80">
|
||||
<DateTimeField
|
||||
className="mr-2"
|
||||
disabled={disabled ?? false}
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={invalid}
|
||||
invalid={invalid}
|
||||
value={originalCreatedFrom ?? undefined}
|
||||
onChange={setOriginalCreatedFrom}
|
||||
onBlur={ev => {
|
||||
const v = ev.target.value
|
||||
if (!(v))
|
||||
return
|
||||
<div className="my-1 flex min-w-0 flex-wrap items-center gap-2">
|
||||
<DateTimeField
|
||||
className="min-w-0 max-w-full"
|
||||
disabled={disabled ?? false}
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={invalid}
|
||||
invalid={invalid}
|
||||
value={originalCreatedFrom ?? undefined}
|
||||
onChange={setOriginalCreatedFrom}
|
||||
onBlur={ev => {
|
||||
const v = ev.target.value
|
||||
if (!(v))
|
||||
return
|
||||
|
||||
const d = new Date (v)
|
||||
if (d.getMinutes () === 0 && d.getHours () === 0)
|
||||
d.setDate (d.getDate () + 1)
|
||||
else
|
||||
d.setMinutes (d.getMinutes () + 1)
|
||||
setOriginalCreatedBefore (toMinutePrecisionIsoUtc (d.toISOString ()))
|
||||
}}/>
|
||||
const d = new Date (v)
|
||||
if (d.getMinutes () === 0 && d.getHours () === 0)
|
||||
d.setDate (d.getDate () + 1)
|
||||
else
|
||||
d.setMinutes (d.getMinutes () + 1)
|
||||
setOriginalCreatedBefore (toMinutePrecisionIsoUtc (d.toISOString ()))
|
||||
}}/>
|
||||
<span className="ainu-ui-label shrink-0 whitespace-nowrap">
|
||||
{label ('labels.since')}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-gray-600 text-white rounded"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setOriginalCreatedFrom (null)
|
||||
}}>
|
||||
{label ('actions.reset')}
|
||||
</Button>
|
||||
</div>
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
className="ainu-ui-label shrink-0 rounded bg-gray-600 text-white
|
||||
dark:bg-gray-600 dark:text-white"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setOriginalCreatedFrom (null)
|
||||
}}>
|
||||
{label ('actions.reset')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="my-1 flex">
|
||||
<div className="w-80">
|
||||
<DateTimeField
|
||||
className="mr-2"
|
||||
disabled={disabled}
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={invalid}
|
||||
invalid={invalid}
|
||||
value={originalCreatedBefore ?? undefined}
|
||||
onChange={setOriginalCreatedBefore}/>
|
||||
<div className="my-1 flex min-w-0 flex-wrap items-center gap-2">
|
||||
<DateTimeField
|
||||
className="min-w-0 max-w-full"
|
||||
disabled={disabled}
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={invalid}
|
||||
invalid={invalid}
|
||||
value={originalCreatedBefore ?? undefined}
|
||||
onChange={setOriginalCreatedBefore}/>
|
||||
<span className="ainu-ui-label shrink-0 whitespace-nowrap">
|
||||
{label ('labels.before')}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-gray-600 text-white rounded"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setOriginalCreatedBefore (null)
|
||||
}}>
|
||||
{label ('actions.reset')}
|
||||
</Button>
|
||||
</div>
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
className="ainu-ui-label shrink-0 rounded bg-gray-600 text-white
|
||||
dark:bg-gray-600 dark:text-white"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setOriginalCreatedBefore (null)
|
||||
}}>
|
||||
{label ('actions.reset')}
|
||||
</Button>
|
||||
</div>
|
||||
</>)}
|
||||
</FormField>)
|
||||
|
||||
@@ -8,13 +8,15 @@ import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import { wikiKeys } from '@/lib/queryKeys'
|
||||
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 { Components } from 'react-markdown'
|
||||
|
||||
type Props = { title: string
|
||||
body?: string }
|
||||
body?: string
|
||||
language?: string }
|
||||
|
||||
const mdComponents = { a: (({ href, children }) => (
|
||||
['/', '.'].some (e => href?.startsWith (e))
|
||||
@@ -27,23 +29,32 @@ const mdComponents = { a: (({ href, children }) => (
|
||||
</a>))) } as const satisfies Components
|
||||
|
||||
|
||||
const WikiBody: FC<Props> = ({ title, body }) => {
|
||||
const WikiBody: FC<Props> = ({ title, body, language = 'und' }) => {
|
||||
const { message } = useUITranslation ('wiki')
|
||||
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 ({
|
||||
enabled: Boolean (body),
|
||||
queryKey: wikiKeys.index ({ }, locale),
|
||||
queryFn: () => fetchWikiPages ({ }, locale) })
|
||||
const pageNames = (data ?? []).map (page => page.title).sort ((a, b) => b.length - a.length)
|
||||
queryKey: wikiKeys.autolinkCandidates (matchLanguage, locale),
|
||||
queryFn: () => fetchWikiAutolinkCandidates (matchLanguage, locale) })
|
||||
|
||||
const remarkPlugins = useMemo (
|
||||
() => [() => remarkWikiAutoLink (pageNames), remarkGFM], [pageNames])
|
||||
() => [() => remarkWikiAutoLink (data ?? [], matchLanguage), remarkGFM],
|
||||
[data, matchLanguage])
|
||||
const components = {
|
||||
...((contentLanguage === 'ain' || contentLanguage === 'ain-Kana')
|
||||
? ainuMDXComponents : { }),
|
||||
...mdComponents }
|
||||
|
||||
return (
|
||||
<ReactMarkdown components={mdComponents} remarkPlugins={remarkPlugins}>
|
||||
{body || message ('missingPage', {
|
||||
url: `/wiki/new?title=${ encodeURIComponent (title) }` }).text}
|
||||
</ReactMarkdown>)
|
||||
<LocalisedDocument language={contentLanguage}>
|
||||
<ReactMarkdown components={components} remarkPlugins={remarkPlugins}>
|
||||
{body || missingPage.text}
|
||||
</ReactMarkdown>
|
||||
</LocalisedDocument>)
|
||||
}
|
||||
|
||||
export default WikiBody
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする