ぼざクリ タグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

105 lines
3.0 KiB

  1. import axios from 'axios'
  2. import toCamel from 'camelcase-keys'
  3. import { useEffect, useState } from 'react'
  4. import ReactMarkdown from 'react-markdown'
  5. import { Link } from 'react-router-dom'
  6. import remarkGFM from 'remark-gfm'
  7. import SectionTitle from '@/components/common/SectionTitle'
  8. import SubsectionTitle from '@/components/common/SubsectionTitle'
  9. import { API_BASE_URL } from '@/config'
  10. import type { Components } from 'react-markdown'
  11. import type { WikiPage } from '@/types'
  12. type Props = { title: string
  13. body?: string }
  14. const mdComponents = { h1: ({ children }) => <SectionTitle>{children}</SectionTitle>,
  15. h2: ({ children }) => <SubsectionTitle>{children}</SubsectionTitle>,
  16. ol: ({ children }) => <ol className="list-decimal pl-6">{children}</ol>,
  17. ul: ({ children }) => <ul className="list-disc pl-6">{children}</ul>,
  18. a: (({ href, children }) => (
  19. ['/', '.'].some (e => href?.startsWith (e))
  20. ? <Link to={href!}>{children}</Link>
  21. : (
  22. <a href={href}
  23. target="_blank"
  24. rel="noopener noreferrer">
  25. {children}
  26. </a>))) } as const satisfies Components
  27. export default ({ title, body }: Props) => {
  28. const [pageNames, setPageNames] = useState<string[]> ([])
  29. const [realBody, setRealBody] = useState<string> ('')
  30. useEffect (() => {
  31. if (!(body))
  32. return
  33. void (async () => {
  34. try
  35. {
  36. const res = await axios.get (`${ API_BASE_URL }/wiki`)
  37. const data = toCamel (res.data as any, { deep: true }) as WikiPage[]
  38. setPageNames (data.map (page => page.title).sort ((a, b) => b.length - a.length))
  39. }
  40. catch
  41. {
  42. setPageNames ([])
  43. }
  44. }) ()
  45. }, [])
  46. useEffect (() => {
  47. setRealBody ('')
  48. }, [body])
  49. useEffect (() => {
  50. if (!(body))
  51. return
  52. const matchIndices = (target: string, keyword: string) => {
  53. const indices: number[] = []
  54. let pos = 0
  55. let idx
  56. while ((idx = target.indexOf (keyword, pos)) >= 0)
  57. {
  58. indices.push (idx)
  59. pos = idx + keyword.length
  60. }
  61. return indices
  62. }
  63. const linkIndices = (text: string, names: string[]): [string, [number, number]][] => {
  64. const result: [string, [number, number]][] = []
  65. names.forEach (name => {
  66. matchIndices (text, name).forEach (idx => {
  67. const start = idx
  68. const end = idx + name.length
  69. const overlaps = result.some (([, [st, ed]]) => start < ed && end > st)
  70. if (!(overlaps))
  71. result.push ([name, [start, end]])
  72. })
  73. })
  74. return result.sort (([, [a]], [, [b]]) => b - a)
  75. }
  76. setRealBody (
  77. linkIndices (body, pageNames).reduce ((acc, [name, [start, end]]) => (
  78. acc.slice (0, start)
  79. + `[${ name }](/wiki/${ encodeURIComponent (name) })`
  80. + acc.slice (end)), body))
  81. }, [body, pageNames])
  82. return (
  83. <ReactMarkdown components={mdComponents} remarkPlugins={[remarkGFM]}>
  84. {realBody || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`}
  85. </ReactMarkdown>)
  86. }