143 lines
6.8 KiB
TypeScript
143 lines
6.8 KiB
TypeScript
import type { LucideIcon } from 'lucide-react'
|
|
import type { CSSProperties, ReactElement } from 'react'
|
|
import type { NameMap } from './displayNames'
|
|
import type { Highlight, OccurrenceStyle } from './occurrences'
|
|
import type { ReadableStore } from './store'
|
|
import type { GroupStyle, Inline, Segment, SegmentGroup, ShownName } from './types'
|
|
import { createContext, memo, useContext, useState } from 'react'
|
|
import { sameShownName, shownName } from './displayNames'
|
|
import { KIND_ICON_COMPONENTS } from './kindIcons'
|
|
import { isCurrent, segmentOccurrence, sourceOf } from './occurrences'
|
|
import { staticStore, useStore } from './store'
|
|
|
|
/** 页面上文字的两样标记:中文名(下标 → 显示成什么),眼下高亮着的名字(没有高亮时没有)。 */
|
|
export interface TextMarks {
|
|
names: NameMap
|
|
highlight?: Highlight
|
|
}
|
|
|
|
/**
|
|
* 文字的标记从哪份 store 读:页面上是 App 的那份(中文名、单击高亮都记在里面);定义卡片浮窗是那个文件的中文名,钉住的行不画高亮。
|
|
* 每段文字只订阅自己那几段的标记。
|
|
*/
|
|
export const TextMarksContext = createContext<ReadableStore<TextMarks>>(staticStore({ names: new Map() }))
|
|
|
|
/**
|
|
* 按词性给每一段上色,颜色在 codeReader.css 的 .cr-role-* 里按主题切换,带自己颜色(color)的段用它(.cr-colored);带源码下标的名字记在 data-offset 上
|
|
* (讲别处名字的段记那个名字的下标,见 sourceOf),跳到定义、高亮各处出现用;是眼下高亮着的名字的一处出现时 data-occurrence 标出底色,是当前那一处(点中的、用键盘跳到的)时
|
|
* 再标 data-current。起了中文名的名字显示中文名(data-name),悬停看原名,定义处本身在后面淡淡写原名;下标照旧,跳到定义、高亮照常。
|
|
* 带悬停提示(tooltip)的段悬停看它。
|
|
* 框起来的一组词画成小框,框里的词同样处理。
|
|
* 中文名和高亮只订阅自己这几段的(TextMarksContext):中文名到了、换了高亮的名字,只有字或底色变了的那几串文字重画。
|
|
*/
|
|
export const SegmentText = memo(({ segments }: { segments: Inline[] }): ReactElement => {
|
|
const marks = useStore(useContext(TextMarksContext), current => segments.map(piece => 'role' in piece ? markOf(piece, current) : undefined), sameMarks)
|
|
return (
|
|
<>
|
|
{segments.map((piece, index) => {
|
|
if (!('role' in piece))
|
|
return <GroupBox key={index} group={piece} />
|
|
const mark = marks[index]
|
|
const shown = mark?.shown
|
|
return (
|
|
<span
|
|
key={index}
|
|
className={piece.color ? `cr-role-${piece.role} cr-colored` : `cr-role-${piece.role}`}
|
|
style={piece.color && { '--color-dark': piece.color.dark, '--color-light': piece.color.light } as CSSProperties}
|
|
data-offset={sourceOf(piece)?.start}
|
|
data-occurrence={mark?.occurrence}
|
|
data-current={mark?.current ? '' : undefined}
|
|
data-name={shown?.name}
|
|
data-tooltip={shown ? `原名 ${piece.text}` : piece.tooltip}
|
|
>
|
|
{shown ? shown.name : piece.text}
|
|
{shown?.definition && <span className="cr-original">{piece.text}</span>}
|
|
</span>
|
|
)
|
|
})}
|
|
</>
|
|
)
|
|
})
|
|
SegmentText.displayName = 'SegmentText'
|
|
|
|
/** 一段的标记:显示成的中文名,高亮里画哪种底色、是不是当前那一处;都没有时没有。 */
|
|
interface SegmentMark {
|
|
shown?: ShownName
|
|
occurrence?: OccurrenceStyle
|
|
current: boolean
|
|
}
|
|
|
|
function markOf(piece: Segment, { names, highlight }: TextMarks): SegmentMark | undefined {
|
|
const shown = shownName(piece, names)
|
|
const occurrence = highlight && segmentOccurrence(piece, highlight.occurrences)
|
|
const current = highlight !== undefined && isCurrent(piece, highlight)
|
|
return shown || occurrence || current ? { shown, occurrence, current } : undefined
|
|
}
|
|
|
|
function sameMarks(a: (SegmentMark | undefined)[], b: (SegmentMark | undefined)[]): boolean {
|
|
return a.length === b.length && a.every((mark, index) => {
|
|
const other = b[index]
|
|
return mark === other || (mark !== undefined && other !== undefined && sameShownName(mark.shown, other.shown) && mark.occurrence === other.occurrence && mark.current === other.current)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 框的样式 → 框头图标:遍历同「循环」块、按条件同「条件」块的图标;条件的一支、小标签、类名不带图标。
|
|
* 颜色在 codeReader.css 的 .cr-group[data-style] 里,框自己给了颜色(color)时用它。
|
|
*/
|
|
const GROUP_ICONS: Record<GroupStyle, LucideIcon | undefined> = {
|
|
iteration: KIND_ICON_COMPONENTS.repeat,
|
|
condition: KIND_ICON_COMPONENTS.split,
|
|
branch: undefined,
|
|
tag: undefined,
|
|
classes: undefined,
|
|
}
|
|
|
|
/** 框自己的颜色,暗色、亮色两个都写在 style 上,CSS 按主题挑一个(见 .cr-group[data-colored])。 */
|
|
function groupColor(group: SegmentGroup): CSSProperties | undefined {
|
|
return group.color && { '--group-dark': group.color.dark, '--group-light': group.color.light } as CSSProperties
|
|
}
|
|
|
|
/**
|
|
* 框起来的一组词:嵌在前后文字里的小框,放不下时整个挪到下一行,比一行还长时在框里换行。框头是图标和 label,只有图标、只有字、都没有都行;
|
|
* 收起来的类名另见 ClassesChip。
|
|
*/
|
|
function GroupBox({ group }: { group: SegmentGroup }): ReactElement {
|
|
if (group.style === 'classes')
|
|
return <ClassesChip group={group} />
|
|
const Icon = GROUP_ICONS[group.style]
|
|
return (
|
|
<span className="cr-group" data-style={group.style} data-colored={group.color ? '' : undefined} style={groupColor(group)}>
|
|
{(Icon || group.label) && (
|
|
<span className="cr-group-head" data-bare={group.label ? undefined : ''}>
|
|
{Icon && <Icon size={12} strokeWidth={2} aria-hidden />}
|
|
{group.label}
|
|
</span>
|
|
)}
|
|
<SegmentText segments={group.segments} />
|
|
</span>
|
|
)
|
|
}
|
|
|
|
/** 收起来的一串类名(「样式 N 个类」):悬停看全部类名,点一下在它下面弹出、盖住后面的字,再点收起。 */
|
|
function ClassesChip({ group }: { group: SegmentGroup }): ReactElement {
|
|
const [open, setOpen] = useState(false)
|
|
return (
|
|
// 点小标、点弹层里的字都不往外传:大纲里点这一行的字会折叠它的子行,点小标只开关类名
|
|
<span className="cr-classes" onClick={event => event.stopPropagation()}>
|
|
<button
|
|
type="button"
|
|
className="cr-group"
|
|
data-style="classes"
|
|
data-open={open ? '' : undefined}
|
|
aria-expanded={open}
|
|
data-tooltip={open ? undefined : group.detail}
|
|
onClick={() => setOpen(value => !value)}
|
|
>
|
|
<span className="cr-group-head">{group.label}</span>
|
|
</button>
|
|
{open && <span className="cr-classes-popup" role="note">{group.detail}</span>}
|
|
</span>
|
|
)
|
|
}
|