first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,190 @@
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
useCodeMirrorStateContext,
useCodeMirrorViewContext,
} from '../codemirror-context'
import {
closeCommandTooltip,
resolveCommandNode,
} from '../../extensions/command-tooltip'
import {
LiteralArgContent,
ShortArg,
ShortTextArgument,
UrlArgument,
} from '../../lezer-latex/latex.terms.mjs'
import { EditorState } from '@codemirror/state'
import { openURL } from '@/features/source-editor/utils/url'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
import OLForm from '@/features/ui/components/ol/ol-form'
import OLButton from '@/features/ui/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
export const HrefTooltipContent: FC = () => {
const state = useCodeMirrorStateContext()
const view = useCodeMirrorViewContext()
const [url, setUrl] = useState<string>(() => readUrl(state) ?? '')
const { t } = useTranslation()
const inputRef = useRef<HTMLInputElement | null>(null)
// Update the URL if the argument value changes while not editing
// TODO: on input blur, update the input value with this URL or read from the syntax tree?
useEffect(() => {
if (inputRef.current) {
const controller = new AbortController()
// update the input URL when it changes in the doc
inputRef.current.addEventListener(
'value-update',
event => {
setUrl((event as CustomEvent<string>).detail)
},
{ signal: controller.signal }
)
// focus the URL input element when the tooltip opens, if the view is focused,
// there is content selected in the doc, and no URL has been entered
if (view.hasFocus && !view.state.selection.main.empty) {
const currentUrl = readUrl(view.state)
if (!currentUrl) {
inputRef.current.focus()
}
}
inputRef.current?.addEventListener(
'blur',
() => {
const currentUrl = readUrl(view.state)
if (currentUrl) {
setUrl(currentUrl)
}
},
{ signal: controller.signal }
)
return () => controller.abort()
}
}, [view])
const handleSubmit = useCallback(
event => {
event.preventDefault()
view.dispatch(closeCommandTooltip())
view.focus()
},
[view]
)
return (
<div className="ol-cm-command-tooltip-content">
<OLForm className="ol-cm-command-tooltip-form" onSubmit={handleSubmit}>
<OLFormGroup controlId="link-tooltip-url-input">
<OLFormLabel>URL</OLFormLabel>
<OLFormControl
type="url"
size="sm"
htmlSize={50}
placeholder="https://…"
value={url}
ref={(element: HTMLInputElement) => {
inputRef.current = element
}}
autoComplete="off"
onChange={event => {
const url = (event.target as HTMLInputElement).value
setUrl(url)
const spec = replaceUrl(state, url)
if (spec) {
view.dispatch(spec)
}
}}
disabled={state.readOnly}
/>
</OLFormGroup>
</OLForm>
<OLButton
variant="link"
type="button"
className="ol-cm-command-tooltip-link justify-content-start"
onClick={() => {
// TODO: unescape content
openURL(url)
}}
>
<MaterialIcon type="open_in_new" />
{t('open_link')}
</OLButton>
{!state.readOnly && (
<OLButton
variant="link"
type="button"
className="ol-cm-command-tooltip-link justify-content-start"
onClick={() => {
const spec = removeLink(state)
if (spec) {
view.dispatch(spec)
view.focus()
}
}}
>
<MaterialIcon type="link_off" />
{t('remove_link')}
</OLButton>
)}
</div>
)
}
const readUrl = (state: EditorState) => {
const commandNode = resolveCommandNode(state)
const argumentNode = commandNode
?.getChild(UrlArgument)
?.getChild(LiteralArgContent)
if (argumentNode) {
return state.sliceDoc(argumentNode.from, argumentNode.to)
}
}
const replaceUrl = (state: EditorState, url: string) => {
const commandNode = resolveCommandNode(state)
const argumentNode = commandNode
?.getChild(UrlArgument)
?.getChild(LiteralArgContent)
if (argumentNode) {
return {
changes: {
from: argumentNode.from,
to: argumentNode.to,
insert: url,
},
}
}
}
const removeLink = (state: EditorState) => {
const commandNode = resolveCommandNode(state)
const contentNode = commandNode
?.getChild(ShortTextArgument)
?.getChild(ShortArg)
if (commandNode && contentNode) {
const content = state.sliceDoc(contentNode.from, contentNode.to)
return {
changes: {
from: commandNode.from,
to: commandNode.to,
insert: content,
},
}
}
}

View File

@@ -0,0 +1,24 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useIncludedFile } from '@/features/source-editor/hooks/use-included-file'
import MaterialIcon from '@/shared/components/material-icon'
import OLButton from '@/features/ui/components/ol/ol-button'
export const IncludeTooltipContent: FC = () => {
const { t } = useTranslation()
const { openIncludedFile } = useIncludedFile('IncludeArgument')
return (
<div className="ol-cm-command-tooltip-content">
<OLButton
variant="link"
type="button"
className="ol-cm-command-tooltip-link"
onClick={openIncludedFile}
>
<MaterialIcon type="edit" />
{t('open_file')}
</OLButton>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useIncludedFile } from '@/features/source-editor/hooks/use-included-file'
import OLButton from '@/features/ui/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
export const InputTooltipContent: FC = () => {
const { t } = useTranslation()
const { openIncludedFile } = useIncludedFile('InputArgument')
return (
<div className="ol-cm-command-tooltip-content">
<OLButton
variant="link"
type="button"
className="ol-cm-command-tooltip-link"
onClick={openIncludedFile}
>
<MaterialIcon type="edit" />
{t('open_file')}
</OLButton>
</div>
)
}

View File

@@ -0,0 +1,103 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import {
useCodeMirrorStateContext,
useCodeMirrorViewContext,
} from '../codemirror-context'
import { resolveCommandNode } from '../../extensions/command-tooltip'
import {
LabelArgument,
RefArgument,
ShortArg,
ShortTextArgument,
} from '../../lezer-latex/latex.terms.mjs'
import { SyntaxNode } from '@lezer/common'
import { syntaxTree } from '@codemirror/language'
import {
EditorSelection,
EditorState,
TransactionSpec,
} from '@codemirror/state'
import { EditorView } from '@codemirror/view'
import OLButton from '@/features/ui/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
export const RefTooltipContent: FC = () => {
const { t } = useTranslation()
const view = useCodeMirrorViewContext()
const state = useCodeMirrorStateContext()
return (
<div className="ol-cm-command-tooltip-content">
<OLButton
variant="link"
type="button"
className="ol-cm-command-tooltip-link"
onClick={() => {
const target = readTarget(state)
if (target) {
const labelNode = findTargetLabel(state, target)
// TODO: handle label not found
if (labelNode) {
view.dispatch(selectNode(labelNode))
view.focus()
}
}
}}
>
<MaterialIcon type="link" />
{t('open_target')}
</OLButton>
</div>
)
}
const readTarget = (state: EditorState) => {
const commandNode = resolveCommandNode(state)
const argumentNode = commandNode
?.getChild(RefArgument)
?.getChild(ShortTextArgument)
?.getChild(ShortArg)
if (argumentNode) {
return state.sliceDoc(argumentNode.from, argumentNode.to)
}
}
const findTargetLabel = (state: EditorState, target: string) => {
let labelNode: SyntaxNode | undefined
syntaxTree(state).iterate({
enter(nodeRef) {
if (labelNode) {
return false
}
if (nodeRef.type.is(LabelArgument)) {
const argumentNode = nodeRef.node
.getChild('ShortTextArgument')
?.getChild('ShortArg')
if (argumentNode) {
const label = state.sliceDoc(argumentNode.from, argumentNode.to)
if (label === target) {
labelNode = argumentNode
}
}
}
},
})
return labelNode
}
const selectNode = (node: SyntaxNode): TransactionSpec => {
const selection = EditorSelection.range(node.from, node.to)
return {
selection,
effects: EditorView.scrollIntoView(selection, {
y: 'center',
}),
}
}

View File

@@ -0,0 +1,47 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useCodeMirrorStateContext } from '../codemirror-context'
import { resolveCommandNode } from '../../extensions/command-tooltip'
import {
LiteralArgContent,
UrlArgument,
} from '../../lezer-latex/latex.terms.mjs'
import { EditorState } from '@codemirror/state'
import { openURL } from '@/features/source-editor/utils/url'
import OLButton from '@/features/ui/components/ol/ol-button'
import MaterialIcon from '@/shared/components/material-icon'
export const UrlTooltipContent: FC = () => {
const { t } = useTranslation()
const state = useCodeMirrorStateContext()
return (
<div className="ol-cm-command-tooltip-content">
<OLButton
variant="link"
type="button"
className="ol-cm-command-tooltip-link"
onClick={() => {
const url = readUrl(state)
if (url) {
openURL(url)
}
}}
>
<MaterialIcon type="open_in_new" />
{t('open_link')}
</OLButton>
</div>
)
}
const readUrl = (state: EditorState) => {
const commandNode = resolveCommandNode(state)
const argumentNode = commandNode
?.getChild(UrlArgument)
?.getChild(LiteralArgContent)
if (argumentNode) {
return state.sliceDoc(argumentNode.from, argumentNode.to)
}
}