feat: toggle bold/italic and add underline, smallcaps, link for Typst editor
Build and Deploy Verso / deploy (push) Has been cancelled
Build and Deploy Verso / deploy (push) Has been cancelled
Bold (Ctrl+B) and italic (Ctrl+I) now unwrap when the cursor is already
inside a Strong/Emphasis node. Added #underline[…] and #smallcaps[…]
wrap commands (toolbar only) and #link("")[…] with Ctrl+K shortcut that
places the cursor in the URL field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { EditorSelection } from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { syntaxTree } from '@codemirror/language'
|
||||
import { matchingAncestor } from '../utils/tree-operations/ancestors'
|
||||
import { type TypstFormattingNode } from '../utils/tree-operations/formatting'
|
||||
import { wrapRanges } from './ranges'
|
||||
|
||||
/**
|
||||
* Toggle Typst symmetric markup (e.g. *bold* or _italic_).
|
||||
*
|
||||
* If the cursor/selection is already inside a matching syntax node the
|
||||
* surrounding markers are removed; otherwise the selection is wrapped.
|
||||
*/
|
||||
export const toggleTypstMarkup =
|
||||
(marker: string, nodeTypeName: TypstFormattingNode) =>
|
||||
(view: EditorView): boolean => {
|
||||
if (view.state.readOnly) return false
|
||||
|
||||
view.dispatch(
|
||||
view.state.changeByRange(range => {
|
||||
const tree = syntaxTree(view.state)
|
||||
const node = tree.resolveInner(range.from, -1)
|
||||
const formattingNode = matchingAncestor(
|
||||
node,
|
||||
n => n.type.name === nodeTypeName
|
||||
)
|
||||
|
||||
if (formattingNode) {
|
||||
const mLen = marker.length
|
||||
const fFrom = formattingNode.from
|
||||
const fTo = formattingNode.to
|
||||
|
||||
// Adjust the selection for the two deletions applied simultaneously.
|
||||
// Positions after fFrom shift left by mLen (opening deletion).
|
||||
// Positions at or after fTo shift left by another mLen (closing deletion).
|
||||
const newFrom =
|
||||
range.from > fFrom ? range.from - mLen : range.from
|
||||
const newTo =
|
||||
range.to -
|
||||
(range.to > fFrom ? mLen : 0) -
|
||||
(range.to >= fTo ? mLen : 0)
|
||||
|
||||
return {
|
||||
changes: [
|
||||
{ from: fFrom, to: fFrom + mLen, insert: '' },
|
||||
{ from: fTo - mLen, to: fTo, insert: '' },
|
||||
],
|
||||
range: range.empty
|
||||
? EditorSelection.cursor(newFrom)
|
||||
: EditorSelection.range(newFrom, newTo),
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap: insert markers around the selection
|
||||
const content = view.state.sliceDoc(range.from, range.to)
|
||||
return {
|
||||
changes: {
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
insert: `${marker}${content}${marker}`,
|
||||
},
|
||||
range: range.empty
|
||||
? EditorSelection.cursor(range.from + marker.length)
|
||||
: EditorSelection.range(
|
||||
range.from + marker.length,
|
||||
range.to + marker.length
|
||||
),
|
||||
}
|
||||
}),
|
||||
{ scrollIntoView: true }
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
// Wraps selection in #link("")[…] and places the cursor in the URL field.
|
||||
// Prefix breakdown: # l i n k ( " " ) [ = 10 chars; URL slot is at offset 7.
|
||||
export const wrapTypstLink = wrapRanges(
|
||||
'#link("")[',
|
||||
']',
|
||||
false,
|
||||
range => EditorSelection.cursor(range.from - 3)
|
||||
)
|
||||
@@ -216,6 +216,34 @@ export const ToolbarItems: FC<{
|
||||
icon="format_italic"
|
||||
shortcut={isMac ? '⌘I' : 'Ctrl+I'}
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-format-underline"
|
||||
label={t('toolbar_underline')}
|
||||
command={commands.wrapTypstUnderline}
|
||||
icon="format_underlined"
|
||||
/>
|
||||
<ToolbarButton
|
||||
id="toolbar-format-smallcaps"
|
||||
label={t('toolbar_smallcaps')}
|
||||
command={commands.wrapTypstSmallcaps}
|
||||
icon="Sc"
|
||||
textIcon
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showGroup('group-misc') && (
|
||||
<div
|
||||
className="ol-cm-toolbar-button-group"
|
||||
data-overflow="group-misc"
|
||||
aria-label={t('toolbar_insert_misc')}
|
||||
>
|
||||
<ToolbarButton
|
||||
id="toolbar-typst-href"
|
||||
label={t('toolbar_insert_link')}
|
||||
command={commands.wrapTypstLink}
|
||||
icon="add_link"
|
||||
shortcut={isMac ? '⌘K' : 'Ctrl+K'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
} from '@codemirror/search'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import { toggleRanges, wrapRanges } from '../../commands/ranges'
|
||||
import {
|
||||
toggleTypstMarkup,
|
||||
wrapTypstLink,
|
||||
} from '../../commands/typst-ranges'
|
||||
import {
|
||||
ancestorListType,
|
||||
toggleListForRanges,
|
||||
@@ -25,8 +29,11 @@ import { sendSearchEvent } from '@/features/event-tracking/search-events'
|
||||
|
||||
export const toggleBold = toggleRanges('\\textbf')
|
||||
export const toggleItalic = toggleRanges('\\textit')
|
||||
export const toggleTypstBold = wrapRanges('*', '*')
|
||||
export const toggleTypstItalic = wrapRanges('_', '_')
|
||||
export const toggleTypstBold = toggleTypstMarkup('*', 'Strong')
|
||||
export const toggleTypstItalic = toggleTypstMarkup('_', 'Emphasis')
|
||||
export const wrapTypstUnderline = wrapRanges('#underline[', ']')
|
||||
export const wrapTypstSmallcaps = wrapRanges('#smallcaps[', ']')
|
||||
export { wrapTypstLink }
|
||||
|
||||
// TODO: apply as a snippet?
|
||||
// TODO: read URL from clipboard?
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Prec } from '@codemirror/state'
|
||||
import { keymap } from '@codemirror/view'
|
||||
import { wrapRanges } from '../../commands/ranges'
|
||||
import { toggleTypstMarkup, wrapTypstLink } from '../../commands/typst-ranges'
|
||||
|
||||
export const shortcuts = () => {
|
||||
return Prec.high(
|
||||
@@ -9,13 +9,19 @@ export const shortcuts = () => {
|
||||
key: 'Ctrl-b',
|
||||
mac: 'Mod-b',
|
||||
preventDefault: true,
|
||||
run: wrapRanges('*', '*'),
|
||||
run: toggleTypstMarkup('*', 'Strong'),
|
||||
},
|
||||
{
|
||||
key: 'Ctrl-i',
|
||||
mac: 'Mod-i',
|
||||
preventDefault: true,
|
||||
run: wrapRanges('_', '_'),
|
||||
run: toggleTypstMarkup('_', 'Emphasis'),
|
||||
},
|
||||
{
|
||||
key: 'Ctrl-k',
|
||||
mac: 'Mod-k',
|
||||
preventDefault: true,
|
||||
run: wrapTypstLink,
|
||||
},
|
||||
])
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user