import hljs from 'highlight.js/lib/core' import json from 'highlight.js/lib/languages/json' import { marked } from 'marked' import { route } from 'preact-router' import { useCallback, useEffect, useMemo, useState } from 'preact/hooks' import { Ad, Btn, ChangelogTag, Footer, Giscus, Octicon, VersionSwitcher } from '../components/index.js' import config from '../Config.js' import { useLocale, useTitle, useVersion } from '../contexts/index.js' import { useActiveTimeout, useAsync, useHash } from '../hooks/index.js' import type { VersionId } from '../services/index.js' import { parseFrontMatter, versionContent } from '../Utils.js' const HASH = '' hljs.registerLanguage('json', json) marked.use({ highlight: (code, lang) => { if (lang === '') return undefined return hljs.highlight(code, { language: lang }).value }, }) interface Props { path?: string id?: string } export function Guide({ id }: Props) { const { locale } = useLocale() const { version, changeVersion } = useVersion() const { changeTitle } = useTitle() const { value: content, refresh } = useAsync(async () => { const res = await fetch(`../../guides/${id}.md`) return await res.text() }, [id]) if ((import.meta as any).hot) { (import.meta as any).hot.on('guide-update', (updateId: string) => { if (id === updateId) refresh() }) } const frontMatter = useMemo(() => { if (!content) return undefined const data = parseFrontMatter(content) changeTitle(data?.title, data?.versions) return data }, [content]) const allowedVersions = useMemo(() => { const orderedVersions = config.versions.map(v => v.id) return (frontMatter?.versions as VersionId[]) ?.sort((a, b) => orderedVersions.indexOf(b) - orderedVersions.indexOf(a)) }, [frontMatter?.versions]) const guideVersion = useMemo(() => { if (!allowedVersions) return version if (allowedVersions.includes(version)) return version return allowedVersions[0] }, [version, frontMatter?.versions]) const html = useMemo(() => { if (!content) return undefined const headings: marked.Tokens.Heading[] = [] let insertedToc = false marked.use({ extensions: [ { name: 'styledCode', level: 'inline', start(src) { return src.match(/\b[fsnj]`/)?.index ?? -1 }, tokenizer(src) { const match = src.match(/^([fsnj])`([^`]+)`/) if (match) { return { type: 'styledCode', raw: match[0], prefix: match[1], text: match[2], } } return undefined }, renderer(token) { let content = token.text let c = { f: 'hljs-attr', s: 'hljs-string', n: 'hljs-number', }[token.prefix as string] if (token.prefix === 'j') { content = hljs.highlight('json', token.text).value c = 'language-json' } return `${content}` }, }, ], walkTokens(token) { if (token.type === 'heading') { headings.push(token) } }, renderer: { link(href, title, text) { if (href === null) return text const title2 = title ? ` title="${title}"` : '' const target = href?.match(/^https?:\/\//) ? ' target="_blank"' : '' return `${text}` }, heading(text, level, raw, slugger) { let toc = '' if (!insertedToc) { toc = `
    ${headings.filter(t => t.depth === 2).map(t => { const id = slugger.slug(t.raw.match(/^#+ (.*)/)?.[1] ?? '', { dryrun: true }) const text = t.text.replaceAll('`', '') return `
  1. ${text}
  2. ` }).join('')}
` insertedToc = true } const id = slugger.slug(raw) const link = `${HASH}` return `${toc}${link}${text}` }, }, }) const guide = content.substring(content.indexOf('---', 3) + 3) const versionedContent = versionContent(guide, guideVersion) return marked(versionedContent, { version: '1.19' } as any) }, [guideVersion, content]) const [hash, setHash] = useHash() const scrollToHeading = useCallback(() => { if (!html) return const heading = document.querySelector(`[id=guide-${hash.slice(1)}]`) if (heading) { const top = heading.getBoundingClientRect().top + window.scrollY window.scrollTo({ top: top - 68, behavior: 'smooth' }) } }, [html, hash]) useEffect(() => { scrollToHeading() }, [html === undefined, hash]) const clickGuideContent = useCallback((e: MouseEvent) => { if (!(e.target instanceof HTMLSpanElement)) return const targetHash = '#' + e.target.id.replace(/^guide-/, '') changeVersion(version, false, true) setHash(targetHash) if (targetHash === hash) { scrollToHeading() } }, [scrollToHeading, hash, version]) const [shareActive, shareSuccess] = useActiveTimeout() const onShare = useCallback(() => { const url = `${location.origin}/guides/${id}/?version=${version}` navigator.clipboard.writeText(url) shareSuccess() }, [id, version]) const onClickTag = useCallback((tag: string) => { route(`/guides/?tags=${tag}`) }, []) const [largeWidth] = useState(window.innerWidth > 600) return
{(frontMatter?.tags && frontMatter.tags.length > 0) &&
{frontMatter.tags.map((tag: string) => onClickTag(tag)} /> )}
} {html && <>
}
}