このコミットが含まれているのは:
2026-10-12 14:46:19 +09:00
コミット 4813a3a09e
11個のファイルの変更、566行の追加、110行の削除
+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) }
}
+70 -1
ファイルの表示
@@ -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))
+10
ファイルの表示
@@ -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 }
+6 -2
ファイルの表示
@@ -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
}
}