このコミットが含まれているのは:
+138
-4
@@ -18,6 +18,18 @@ export type HelpSearchEntry = HelpEditionText & {
|
||||
identifier: string
|
||||
edition: string }
|
||||
|
||||
export type HelpEditionContent = HelpEditionText & { content: ComponentType }
|
||||
|
||||
export type HelpEditionSource = HelpEditionText & { source: string }
|
||||
export type HelpChange = HelpSearchEntry & { pred: string | null }
|
||||
export type HelpDiffLine = {
|
||||
type: 'added' | 'removed' | 'unchanged'
|
||||
content: string }
|
||||
export type HelpDiff = {
|
||||
from: HelpEditionSource
|
||||
to: HelpEditionSource
|
||||
lines: HelpDiffLine[] }
|
||||
|
||||
const articleFiles = import.meta.glob<string> ('../../help/*/article.yml',
|
||||
{ query: '?raw',
|
||||
import: 'default',
|
||||
@@ -72,6 +84,18 @@ export const getHelpEditions = (
|
||||
}
|
||||
|
||||
|
||||
export const getHelpPublishedEditions = (
|
||||
identifier: string,
|
||||
language: string,
|
||||
): string[] => {
|
||||
const currentEdition = getHelpArticle (identifier)?.languages[language]?.currentEdition
|
||||
return currentEdition == null
|
||||
? []
|
||||
: getHelpEditions (identifier, language)
|
||||
.filter (edition => compareEditions (edition, currentEdition) <= 0)
|
||||
}
|
||||
|
||||
|
||||
export const getHelpEdition = async (
|
||||
identifier: string,
|
||||
language: string,
|
||||
@@ -87,28 +111,40 @@ export const getHelpEdition = async (
|
||||
}
|
||||
|
||||
|
||||
export const getHelpEditionText = async (
|
||||
export const getHelpEditionSource = async (
|
||||
identifier: string,
|
||||
language: string,
|
||||
edition: string,
|
||||
): Promise<HelpEditionText | null> => {
|
||||
): Promise<HelpEditionSource | null> => {
|
||||
const path = `../../help/${ identifier }/editions/${ language }/${ edition }.mdx`
|
||||
const loader = rawEditionFiles[path]
|
||||
|
||||
if (!(loader))
|
||||
return null
|
||||
|
||||
const mdx = (await loader ()).replace (/^\uFEFF/, '')
|
||||
const source = await loader ()
|
||||
const mdx = source.replace (/^\uFEFF/, '')
|
||||
const heading = mdx.match (/^# +([^\r\n]+)(?:\r?\n|$)/)
|
||||
|
||||
if (!(heading))
|
||||
throw new Error (`Help MDX の先頭に H1 がありません: ${ path }`)
|
||||
|
||||
return { title: heading[1].trim (),
|
||||
return { source,
|
||||
title: heading[1].trim (),
|
||||
body: mdx.slice (heading[0].length) }
|
||||
}
|
||||
|
||||
|
||||
export const getHelpEditionText = async (
|
||||
identifier: string,
|
||||
language: string,
|
||||
edition: string,
|
||||
): Promise<HelpEditionText | null> => {
|
||||
const content = await getHelpEditionSource (identifier, language, edition)
|
||||
return content == null ? null : { title: content.title, body: content.body }
|
||||
}
|
||||
|
||||
|
||||
export const getHelpSearchEntries = async (
|
||||
language: string,
|
||||
): Promise<HelpSearchEntry[]> => {
|
||||
@@ -130,3 +166,101 @@ export const getHelpSearchEntries = async (
|
||||
|
||||
return entries.filter ((entry): entry is HelpSearchEntry => entry != null)
|
||||
}
|
||||
|
||||
|
||||
export const getHelpEditionContent = async (
|
||||
identifier: string,
|
||||
language: string,
|
||||
edition: string,
|
||||
): Promise<HelpEditionContent | null> => {
|
||||
const [content, text] = await Promise.all ([
|
||||
getHelpEdition (identifier, language, edition),
|
||||
getHelpEditionText (identifier, language, edition)])
|
||||
|
||||
return content != null && text != null ? { content, ...text } : null
|
||||
}
|
||||
|
||||
|
||||
export const getHelpChanges = async (language: string): Promise<HelpChange[]> => {
|
||||
const articles = Object.values (articleFiles).map (source =>
|
||||
cameliseArticle (parseYAML (source)))
|
||||
const changes = await Promise.all (articles.flatMap (article => {
|
||||
const editions = getHelpPublishedEditions (article.identifier, language)
|
||||
return editions.map (async (edition, index) => {
|
||||
const text = await getHelpEditionText (article.identifier, language, edition)
|
||||
if (text == null)
|
||||
throw new Error (`Help edition が見つかりません: ${ article.identifier }/${ edition }`)
|
||||
|
||||
return { identifier: article.identifier,
|
||||
edition, pred: editions[index - 1] ?? null, ...text }
|
||||
})
|
||||
}))
|
||||
|
||||
return changes.sort ((a, b) =>
|
||||
compareEditions (b.edition, a.edition)
|
||||
|| a.identifier.localeCompare (b.identifier))
|
||||
}
|
||||
|
||||
|
||||
const diffHelpLines = (from: string, to: string): HelpDiffLine[] => {
|
||||
const before = from.match (/[^\n]*\n|[^\n]+$/g) ?? []
|
||||
const after = to.match (/[^\n]*\n|[^\n]+$/g) ?? []
|
||||
const lengths = Array.from ({ length: before.length + 1 },
|
||||
() => new Uint32Array (after.length + 1))
|
||||
|
||||
for (let i = before.length - 1; i >= 0; --i)
|
||||
{
|
||||
for (let j = after.length - 1; j >= 0; --j)
|
||||
{
|
||||
lengths[i][j] =
|
||||
before[i] === after[j]
|
||||
? lengths[i + 1][j + 1] + 1
|
||||
: Math.max (lengths[i + 1][j], lengths[i][j + 1])
|
||||
}
|
||||
}
|
||||
|
||||
const lines: HelpDiffLine[] = []
|
||||
let i = 0
|
||||
let j = 0
|
||||
while (i < before.length || j < after.length)
|
||||
{
|
||||
if (i < before.length && j < after.length && before[i] === after[j])
|
||||
{
|
||||
lines.push ({ type: 'unchanged', content: before[i] })
|
||||
++i
|
||||
++j
|
||||
}
|
||||
else if (i < before.length
|
||||
&& (j === after.length || lengths[i + 1][j] >= lengths[i][j + 1]))
|
||||
{
|
||||
lines.push ({ type: 'removed', content: before[i] })
|
||||
++i
|
||||
}
|
||||
else
|
||||
{
|
||||
lines.push ({ type: 'added', content: after[j] })
|
||||
++j
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
|
||||
export const getHelpDiff = async (
|
||||
identifier: string,
|
||||
language: string,
|
||||
from: string,
|
||||
to: string,
|
||||
): Promise<HelpDiff | null> => {
|
||||
const editions = getHelpPublishedEditions (identifier, language)
|
||||
if (!(editions.includes (from)) || !(editions.includes (to)))
|
||||
return null
|
||||
|
||||
const [before, after] = await Promise.all ([
|
||||
getHelpEditionSource (identifier, language, from),
|
||||
getHelpEditionSource (identifier, language, to)])
|
||||
if (before == null || after == null)
|
||||
return null
|
||||
|
||||
return { from: before, to: after, lines: diffHelpLines (before.source, after.source) }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { match } from 'path-to-regexp'
|
||||
|
||||
import { getHelpArticle,
|
||||
getHelpChanges,
|
||||
getHelpDiff,
|
||||
getHelpEditionContent,
|
||||
getHelpSearchEntries } from '@/lib/help'
|
||||
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
|
||||
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||
import { fetchNicoTags, fetchTagByName, fetchTag, fetchTagChanges, fetchTags } from '@/lib/tags'
|
||||
import { fetchWikiPage,
|
||||
fetchWikiPageByTitle,
|
||||
@@ -15,12 +20,69 @@ type Prefetcher = (qc: QueryClient, url: URL) => Promise<void>
|
||||
const mPost = match<{ id: string }> ('/posts/:id')
|
||||
const mWiki = match<{ title: string }> ('/wiki/:title')
|
||||
const mTag = match<{ id: string }> ('/tags/:id')
|
||||
const mHelp = match<{ identifier: string }> ('/help/:identifier')
|
||||
const mHelpDiff = match<{ identifier: string }> ('/help/:identifier/diff')
|
||||
|
||||
|
||||
const boolFromQuery = (value: string | null): boolean =>
|
||||
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
|
||||
|
||||
|
||||
const prefetchHelpIndex: Prefetcher = async qc => {
|
||||
await qc.prefetchQuery ({
|
||||
queryKey: helpKeys.index ('ja'),
|
||||
queryFn: () => getHelpSearchEntries ('ja'),
|
||||
staleTime: Infinity })
|
||||
}
|
||||
|
||||
|
||||
const prefetchHelpShow: Prefetcher = async (qc, url) => {
|
||||
const matched = mHelp (url.pathname)
|
||||
if (!(matched))
|
||||
return
|
||||
|
||||
const { identifier } = matched.params
|
||||
const language = 'ja'
|
||||
const article = await qc.fetchQuery ({
|
||||
queryKey: helpKeys.article (identifier),
|
||||
queryFn: () => getHelpArticle (identifier),
|
||||
staleTime: Infinity })
|
||||
const edition = url.searchParams.get ('edition') ?? article?.languages[language]?.currentEdition
|
||||
if (edition == null)
|
||||
return
|
||||
|
||||
await qc.prefetchQuery ({
|
||||
queryKey: helpKeys.edition (identifier, language, edition),
|
||||
queryFn: () => getHelpEditionContent (identifier, language, edition),
|
||||
staleTime: Infinity })
|
||||
}
|
||||
|
||||
|
||||
const prefetchHelpChanges: Prefetcher = async qc => {
|
||||
await qc.prefetchQuery ({
|
||||
queryKey: helpKeys.changes ('ja'),
|
||||
queryFn: () => getHelpChanges ('ja'),
|
||||
staleTime: Infinity })
|
||||
}
|
||||
|
||||
|
||||
const prefetchHelpDiff: Prefetcher = async (qc, url) => {
|
||||
const matched = mHelpDiff (url.pathname)
|
||||
const fromValues = url.searchParams.getAll ('from')
|
||||
const toValues = url.searchParams.getAll ('to')
|
||||
const [from] = fromValues
|
||||
const [to] = toValues
|
||||
if (!(matched) || fromValues.length !== 1 || toValues.length !== 1 || !(from) || !(to))
|
||||
return
|
||||
|
||||
const { identifier } = matched.params
|
||||
await qc.prefetchQuery ({
|
||||
queryKey: helpKeys.diff (identifier, 'ja', from, to),
|
||||
queryFn: () => getHelpDiff (identifier, 'ja', from, to),
|
||||
staleTime: Infinity })
|
||||
}
|
||||
|
||||
|
||||
const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => {
|
||||
const title = url.searchParams.get ('title') ?? ''
|
||||
|
||||
@@ -223,6 +285,13 @@ const prefetchTagChanges: Prefetcher = async (qc, url) => {
|
||||
|
||||
|
||||
export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [
|
||||
{ test: u => /^\/help\/?$/.test (u.pathname), run: prefetchHelpIndex },
|
||||
{ test: u => /^\/help\/changes\/?$/.test (u.pathname), run: prefetchHelpChanges },
|
||||
{ test: u => Boolean (mHelpDiff (u.pathname)), run: prefetchHelpDiff },
|
||||
{ test: u => {
|
||||
const matched = mHelp (u.pathname)
|
||||
return Boolean (matched && matched.params.identifier !== 'changes')
|
||||
}, run: prefetchHelpShow },
|
||||
{ test: u => ['/', '/posts', '/posts/search'].includes (u.pathname),
|
||||
run: prefetchPostsIndex },
|
||||
{ test: u => (!(['/posts/new', '/posts/changes', '/posts/search'].includes (u.pathname))
|
||||
|
||||
@@ -61,3 +61,13 @@ export const wikiKeys = {
|
||||
root: ['wiki'] as const,
|
||||
index: (p: { title?: string }) => ['wiki', 'index', p] as const,
|
||||
show: (title: string, p: { version?: string }) => ['wiki', title, p] as const }
|
||||
|
||||
export const helpKeys = {
|
||||
root: ['help'] as const,
|
||||
index: (language: string) => ['help', 'index', language] as const,
|
||||
article: (identifier: string) => ['help', 'article', identifier] as const,
|
||||
edition: (identifier: string, language: string, edition: string) =>
|
||||
['help', 'edition', identifier, language, edition] as const,
|
||||
diff: (identifier: string, language: string, from: string, to: string) =>
|
||||
['help', 'diff', identifier, language, from, to] as const,
|
||||
changes: (language: string) => ['help', 'changes', language] as const }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isAxiosError } from 'axios'
|
||||
|
||||
import { apiGet } from '@/lib/api'
|
||||
|
||||
import type { WikiPage } from '@/types'
|
||||
@@ -21,8 +23,10 @@ export const fetchWikiPageByTitle = async (
|
||||
{
|
||||
return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { params: { version } })
|
||||
}
|
||||
catch
|
||||
catch (error)
|
||||
{
|
||||
return null
|
||||
if (isAxiosError (error) && error.response?.status === 404)
|
||||
return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする