mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 06:03:45 +01:00
Add text formatting commands to the LaTeX grammar (#19607)
GitOrigin-RevId: f69cd323992c80de3f0a458a637fa8f160017076
This commit is contained in:
+31
-41
@@ -1038,11 +1038,38 @@ export const atomicDecorations = (options: Options) => {
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (nodeRef.type.is('$ToggleTextFormattingCommand')) {
|
||||
// markup that can be toggled using toolbar buttons/keyboard shortcuts
|
||||
const textArgumentNode = nodeRef.node.getChild('TextArgument')
|
||||
const argumentText = textArgumentNode?.getChild('LongArg')
|
||||
const shouldShowBraces =
|
||||
!shouldDecorate(state, nodeRef) ||
|
||||
argumentText?.from === argumentText?.to
|
||||
decorations.push(
|
||||
...decorateArgumentBraces(
|
||||
new BraceWidget(shouldShowBraces ? '{' : ''),
|
||||
textArgumentNode,
|
||||
nodeRef.from,
|
||||
true,
|
||||
new BraceWidget(shouldShowBraces ? '}' : '')
|
||||
)
|
||||
)
|
||||
} else if (nodeRef.type.is('$OtherTextFormattingCommand')) {
|
||||
// markup that can't be toggled using toolbar buttons/keyboard shortcuts
|
||||
const textArgumentNode = nodeRef.node.getChild('TextArgument')
|
||||
if (shouldDecorate(state, nodeRef)) {
|
||||
decorations.push(
|
||||
...decorateArgumentBraces(
|
||||
new BraceWidget(),
|
||||
textArgumentNode,
|
||||
nodeRef.from
|
||||
)
|
||||
)
|
||||
}
|
||||
} else if (nodeRef.type.is('UnknownCommand')) {
|
||||
// a command that's not defined separately by the grammar
|
||||
const commandNode = nodeRef.node
|
||||
const commandNameNode = commandNode.getChild('$CtrlSeq')
|
||||
const textArgumentNode = commandNode.getChild('TextArgument')
|
||||
|
||||
if (commandNameNode) {
|
||||
const commandName = state.doc
|
||||
@@ -1050,46 +1077,9 @@ export const atomicDecorations = (options: Options) => {
|
||||
.trim()
|
||||
|
||||
if (commandName.length > 0) {
|
||||
if (
|
||||
// markup that can be toggled using toolbar buttons/keyboard shortcuts
|
||||
['\\textbf', '\\textit', '\\underline'].includes(commandName)
|
||||
) {
|
||||
const argumentText = textArgumentNode?.getChild('LongArg')
|
||||
const shouldShowBraces =
|
||||
!shouldDecorate(state, nodeRef) ||
|
||||
argumentText?.from === argumentText?.to
|
||||
decorations.push(
|
||||
...decorateArgumentBraces(
|
||||
new BraceWidget(shouldShowBraces ? '{' : ''),
|
||||
textArgumentNode,
|
||||
nodeRef.from,
|
||||
true,
|
||||
new BraceWidget(shouldShowBraces ? '}' : '')
|
||||
)
|
||||
)
|
||||
} else if (
|
||||
// markup that can't be toggled using toolbar buttons/keyboard shortcuts
|
||||
[
|
||||
'\\textsc',
|
||||
'\\texttt',
|
||||
'\\textmd',
|
||||
'\\textsf',
|
||||
'\\textsuperscript',
|
||||
'\\textsubscript',
|
||||
'\\sout',
|
||||
'\\emph',
|
||||
].includes(commandName)
|
||||
) {
|
||||
if (shouldDecorate(state, nodeRef)) {
|
||||
decorations.push(
|
||||
...decorateArgumentBraces(
|
||||
new BraceWidget(),
|
||||
textArgumentNode,
|
||||
nodeRef.from
|
||||
)
|
||||
)
|
||||
}
|
||||
} else if (commandName === '\\keywords') {
|
||||
const textArgumentNode = commandNode.getChild('TextArgument')
|
||||
|
||||
if (commandName === '\\keywords') {
|
||||
if (shouldDecorate(state, nodeRef)) {
|
||||
// command name and opening brace
|
||||
decorations.push(
|
||||
|
||||
+113
-100
@@ -2,59 +2,49 @@ import { EditorState } from '@codemirror/state'
|
||||
import { SyntaxNode } from '@lezer/common'
|
||||
import { COMMAND_SUBSTITUTIONS } from '../visual-widgets/character'
|
||||
|
||||
const isUnknownCommandWithName = (
|
||||
node: SyntaxNode,
|
||||
command: string,
|
||||
getText: (from: number, to: number) => string
|
||||
): boolean => {
|
||||
const commandName = getUnknownCommandName(node, getText)
|
||||
if (commandName === undefined) {
|
||||
return false
|
||||
}
|
||||
return commandName === command
|
||||
}
|
||||
|
||||
const getUnknownCommandName = (
|
||||
node: SyntaxNode,
|
||||
getText: (from: number, to: number) => string
|
||||
): string | undefined => {
|
||||
if (!node.type.is('UnknownCommand')) {
|
||||
return undefined
|
||||
}
|
||||
const commandNameNode = node.getChild('CtrlSeq')
|
||||
if (!commandNameNode) {
|
||||
return undefined
|
||||
}
|
||||
const commandName = getText(commandNameNode.from, commandNameNode.to)
|
||||
return commandName
|
||||
}
|
||||
|
||||
type NodeMapping = {
|
||||
type Markup = {
|
||||
elementType: keyof HTMLElementTagNameMap
|
||||
className?: string
|
||||
}
|
||||
type MarkupMapping = {
|
||||
[command: string]: NodeMapping
|
||||
}
|
||||
const MARKUP_COMMANDS: MarkupMapping = {
|
||||
'\\textit': {
|
||||
elementType: 'i',
|
||||
},
|
||||
'\\textbf': {
|
||||
elementType: 'b',
|
||||
},
|
||||
'\\emph': {
|
||||
elementType: 'em',
|
||||
},
|
||||
'\\texttt': {
|
||||
elementType: 'span',
|
||||
className: 'ol-cm-command-texttt',
|
||||
},
|
||||
'\\textsc': {
|
||||
elementType: 'span',
|
||||
className: 'ol-cm-command-textsc',
|
||||
},
|
||||
}
|
||||
|
||||
const textFormattingMarkupMap = new Map<string, Markup>([
|
||||
[
|
||||
'TextBoldCommand', // \\textbf
|
||||
{ elementType: 'b' },
|
||||
],
|
||||
[
|
||||
'TextItalicCommand', // \\textit
|
||||
{ elementType: 'i' },
|
||||
],
|
||||
[
|
||||
'TextSmallCapsCommand', // \\textsc
|
||||
{ elementType: 'span', className: 'ol-cm-command-textsc' },
|
||||
],
|
||||
[
|
||||
'TextTeletypeCommand', // \\texttt
|
||||
{ elementType: 'span', className: 'ol-cm-command-texttt' },
|
||||
],
|
||||
[
|
||||
'TextSuperscriptCommand', // \\textsuperscript
|
||||
{ elementType: 'sup' },
|
||||
],
|
||||
[
|
||||
'TextSubscriptCommand', // \\textsubscript
|
||||
{ elementType: 'sub' },
|
||||
],
|
||||
[
|
||||
'EmphasisCommand', // \\emph
|
||||
{ elementType: 'em' },
|
||||
],
|
||||
[
|
||||
'UnderlineCommand', // \\underline
|
||||
{ elementType: 'span', className: 'ol-cm-command-underline' },
|
||||
],
|
||||
])
|
||||
|
||||
const markupMap = new Map<string, Markup>([
|
||||
['\\and', { elementType: 'span', className: 'ol-cm-command-and' }],
|
||||
])
|
||||
|
||||
/**
|
||||
* Does a small amount of typesetting of LaTeX content into a DOM element.
|
||||
@@ -62,35 +52,44 @@ const MARKUP_COMMANDS: MarkupMapping = {
|
||||
* function if you wish to typeset math content.
|
||||
* @param node The syntax node containing the text to be typeset
|
||||
* @param element The DOM element to typeset into
|
||||
* @param state The editor state where `node` is from
|
||||
* @param getText The editor state where `node` is from or a custom function
|
||||
*/
|
||||
export function typesetNodeIntoElement(
|
||||
node: SyntaxNode,
|
||||
element: HTMLElement,
|
||||
state: EditorState | ((from: number, to: number) => string)
|
||||
getText: EditorState | ((from: number, to: number) => string)
|
||||
) {
|
||||
let getText: (from: number, to: number) => string
|
||||
if (typeof state === 'function') {
|
||||
getText = state
|
||||
} else {
|
||||
getText = state!.sliceDoc.bind(state!)
|
||||
if (getText instanceof EditorState) {
|
||||
getText = getText.sliceDoc.bind(getText)
|
||||
}
|
||||
|
||||
// If we're a TextArgument node, we should skip the braces
|
||||
const argument = node.getChild('LongArg')
|
||||
if (argument) {
|
||||
node = argument
|
||||
}
|
||||
|
||||
const ancestorStack = [element]
|
||||
|
||||
const ancestor = () => ancestorStack[ancestorStack.length - 1]
|
||||
const popAncestor = () => ancestorStack.pop()!
|
||||
const pushAncestor = (x: HTMLElement) => ancestorStack.push(x)
|
||||
const pushAncestor = (element: HTMLElement) => ancestorStack.push(element)
|
||||
|
||||
let from = node.from
|
||||
|
||||
const addMarkup = (markup: Markup, childNode: SyntaxNode) => {
|
||||
const element = document.createElement(markup.elementType)
|
||||
if (markup.className) {
|
||||
element.classList.add(markup.className)
|
||||
}
|
||||
pushAncestor(element)
|
||||
from = chooseFrom(childNode)
|
||||
}
|
||||
|
||||
node.cursor().iterate(
|
||||
function enter(childNodeRef) {
|
||||
const childNode = childNodeRef.node
|
||||
|
||||
if (from < childNode.from) {
|
||||
ancestor().append(
|
||||
document.createTextNode(getText(from, childNode.from))
|
||||
@@ -98,57 +97,43 @@ export function typesetNodeIntoElement(
|
||||
from = childNode.from
|
||||
}
|
||||
|
||||
if (childNode.type.is('UnknownCommand')) {
|
||||
const commandNameNode = childNode.getChild('CtrlSeq')
|
||||
if (commandNameNode) {
|
||||
const commandName = getText(commandNameNode.from, commandNameNode.to)
|
||||
const mapping: NodeMapping | undefined = MARKUP_COMMANDS[commandName]
|
||||
if (mapping) {
|
||||
const element = document.createElement(mapping.elementType)
|
||||
if (mapping.className) {
|
||||
element.classList.add(mapping.className)
|
||||
}
|
||||
pushAncestor(element)
|
||||
const textArgument = childNode.getChild('TextArgument')
|
||||
from = textArgument?.getChild('LongArg')?.from ?? childNode.to
|
||||
return
|
||||
}
|
||||
}
|
||||
// commands defined in the grammar
|
||||
const markup = textFormattingMarkupMap.get(childNode.type.name)
|
||||
if (markup) {
|
||||
addMarkup(markup, childNode)
|
||||
return
|
||||
}
|
||||
if (isUnknownCommandWithName(childNode, '\\and', getText)) {
|
||||
const spanElement = document.createElement('span')
|
||||
spanElement.classList.add('ol-cm-command-and')
|
||||
pushAncestor(spanElement)
|
||||
const textArgument = childNode.getChild('TextArgument')
|
||||
from = textArgument?.getChild('LongArg')?.from ?? childNode.to
|
||||
} else if (
|
||||
isUnknownCommandWithName(childNode, '\\corref', getText) ||
|
||||
isUnknownCommandWithName(childNode, '\\fnref', getText) ||
|
||||
isUnknownCommandWithName(childNode, '\\thanks', getText)
|
||||
) {
|
||||
// ignoring these commands
|
||||
from = childNode.to
|
||||
return false
|
||||
} else if (childNode.type.is('LineBreak')) {
|
||||
ancestor().appendChild(document.createElement('br'))
|
||||
from = childNode.to
|
||||
} else if (childNode.type.is('UnknownCommand')) {
|
||||
const command = getText(childNode.from, childNode.to)
|
||||
const symbol = COMMAND_SUBSTITUTIONS.get(command.trim())
|
||||
if (symbol !== undefined) {
|
||||
|
||||
// commands not defined in the grammar
|
||||
const commandName = unknownCommandName(childNode, getText)
|
||||
if (commandName) {
|
||||
const markup = markupMap.get(commandName)
|
||||
if (markup) {
|
||||
addMarkup(markup, childNode)
|
||||
return
|
||||
}
|
||||
|
||||
if (['\\corref', '\\fnref', '\\thanks'].includes(commandName)) {
|
||||
// ignoring these commands
|
||||
from = childNode.to
|
||||
return false
|
||||
}
|
||||
|
||||
const symbol = COMMAND_SUBSTITUTIONS.get(commandName)
|
||||
if (symbol) {
|
||||
ancestor().append(document.createTextNode(symbol))
|
||||
from = childNode.to
|
||||
return false
|
||||
}
|
||||
} else if (childNode.type.is('LineBreak')) {
|
||||
ancestor().append(document.createElement('br'))
|
||||
from = childNode.to
|
||||
}
|
||||
},
|
||||
function leave(childNodeRef) {
|
||||
const childNode = childNodeRef.node
|
||||
const commandName = getUnknownCommandName(childNode, getText)
|
||||
if (
|
||||
(commandName && Boolean(MARKUP_COMMANDS[commandName])) ||
|
||||
isUnknownCommandWithName(childNode, '\\and', getText)
|
||||
) {
|
||||
|
||||
if (shouldHandleLeave(childNode, getText)) {
|
||||
const typeSetElement = popAncestor()
|
||||
ancestor().appendChild(typeSetElement)
|
||||
const textArgument = childNode.getChild('TextArgument')
|
||||
@@ -159,9 +144,37 @@ export function typesetNodeIntoElement(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (from < node.to) {
|
||||
ancestor().append(document.createTextNode(getText(from, node.to)))
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
const chooseFrom = (node: SyntaxNode) =>
|
||||
node.getChild('TextArgument')?.getChild('LongArg')?.from ?? node.to
|
||||
|
||||
const shouldHandleLeave = (
|
||||
node: SyntaxNode,
|
||||
getText: (from: number, to: number) => string
|
||||
) => {
|
||||
if (textFormattingMarkupMap.has(node.type.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const commandName = unknownCommandName(node, getText)
|
||||
return commandName && markupMap.has(commandName)
|
||||
}
|
||||
|
||||
const unknownCommandName = (
|
||||
node: SyntaxNode,
|
||||
getText: (from: number, to: number) => string
|
||||
): string | undefined => {
|
||||
if (node.type.is('UnknownCommand')) {
|
||||
const commandNameNode = node.getChild('$CtrlSeq')
|
||||
if (commandNameNode) {
|
||||
return getText(commandNameNode.from, commandNameNode.to).trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user