latex-language.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { LRLanguage, foldNodeProp, foldInside } from '@codemirror/language'
  2. import { parser } from '../../lezer-latex/latex.mjs'
  3. import { styleTags, tags as t } from '@lezer/highlight'
  4. import * as termsModule from '../../lezer-latex/latex.terms.mjs'
  5. import { NodeProp } from '@lezer/common'
  6. import {
  7. Tokens,
  8. commentIsOpenFold,
  9. findClosingFoldComment,
  10. getFoldRange,
  11. } from '../../utils/tree-query'
  12. import { closeBracketConfig } from './close-bracket-config'
  13. import { noSpellCheckProp } from '@/features/source-editor/utils/node-props'
  14. const styleOverrides: Record<string, any> = {
  15. DocumentClassCtrlSeq: t.keyword,
  16. UsePackageCtrlSeq: t.keyword,
  17. CiteCtrlSeq: t.keyword,
  18. CiteStarrableCtrlSeq: t.keyword,
  19. RefCtrlSeq: t.keyword,
  20. RefStarrableCtrlSeq: t.keyword,
  21. LabelCtrlSeq: t.keyword,
  22. }
  23. const styleEntry = (token: string, defaultStyle: any) => {
  24. return [token, styleOverrides[token] || defaultStyle]
  25. }
  26. const Styles = {
  27. ctrlSeq: Object.fromEntries(
  28. Tokens.ctrlSeq.map(token => styleEntry(token, t.tagName))
  29. ),
  30. ctrlSym: Object.fromEntries(
  31. Tokens.ctrlSym.map(token => styleEntry(token, t.literal))
  32. ),
  33. envName: Object.fromEntries(
  34. Tokens.envName.map(token => styleEntry(token, t.attributeValue))
  35. ),
  36. }
  37. const typeMap: Record<string, string[]> = {
  38. // commands that are section headings
  39. PartCtrlSeq: ['$SectioningCtrlSeq'],
  40. ChapterCtrlSeq: ['$SectioningCtrlSeq'],
  41. SectionCtrlSeq: ['$SectioningCtrlSeq'],
  42. SubSectionCtrlSeq: ['$SectioningCtrlSeq'],
  43. SubSubSectionCtrlSeq: ['$SectioningCtrlSeq'],
  44. ParagraphCtrlSeq: ['$SectioningCtrlSeq'],
  45. SubParagraphCtrlSeq: ['$SectioningCtrlSeq'],
  46. // commands that have a "command tooltip"
  47. HrefCommand: ['$CommandTooltipCommand'],
  48. Include: ['$CommandTooltipCommand'],
  49. Input: ['$CommandTooltipCommand'],
  50. Ref: ['$CommandTooltipCommand'],
  51. UrlCommand: ['$CommandTooltipCommand'],
  52. // text formatting commands that can be toggled via the toolbar
  53. TextBoldCommand: ['$ToggleTextFormattingCommand'],
  54. TextItalicCommand: ['$ToggleTextFormattingCommand'],
  55. // text formatting commands that cannot be toggled via the toolbar
  56. TextSmallCapsCommand: ['$OtherTextFormattingCommand'],
  57. TextTeletypeCommand: ['$OtherTextFormattingCommand'],
  58. TextMediumCommand: ['$OtherTextFormattingCommand'],
  59. TextSansSerifCommand: ['$OtherTextFormattingCommand'],
  60. TextSuperscriptCommand: ['$OtherTextFormattingCommand'],
  61. TextSubscriptCommand: ['$OtherTextFormattingCommand'],
  62. StrikeOutCommand: ['$OtherTextFormattingCommand'],
  63. EmphasisCommand: ['$OtherTextFormattingCommand'],
  64. UnderlineCommand: ['$OtherTextFormattingCommand'],
  65. }
  66. export const LaTeXLanguage = LRLanguage.define({
  67. name: 'latex',
  68. parser: parser.configure({
  69. props: [
  70. foldNodeProp.add({
  71. Comment: (node, state) => {
  72. if (commentIsOpenFold(node, state)) {
  73. const closingFoldNode = findClosingFoldComment(node, state)
  74. if (closingFoldNode) {
  75. return getFoldRange(node, closingFoldNode, state)
  76. }
  77. }
  78. return null
  79. },
  80. Group: foldInside,
  81. NonEmptyGroup: foldInside,
  82. TextArgument: foldInside,
  83. // TODO: Why isn't
  84. // `Content: node => node,`
  85. // enough? For some reason it doesn't work if there's a newline after
  86. // \section{a}, but works for \section{a}b
  87. $Environment: node => node.getChild('Content'),
  88. $Section: node => {
  89. const BACKWARDS = -1
  90. const lastChild = node.resolveInner(node.to, BACKWARDS)
  91. const content = node.getChild('Content')
  92. if (!content) {
  93. return null
  94. }
  95. if (lastChild.type.is(termsModule.NewLine)) {
  96. // Ignore last newline for sectioning commands
  97. return { from: content!.from, to: lastChild.from }
  98. }
  99. if (lastChild.type.is(termsModule.Whitespace)) {
  100. // If the sectioningcommand is indented on a newline
  101. let sibling = lastChild.prevSibling
  102. while (sibling?.type.is(termsModule.Whitespace)) {
  103. sibling = sibling.prevSibling
  104. }
  105. if (sibling?.type.is(termsModule.NewLine)) {
  106. return { from: content!.from, to: sibling.from }
  107. }
  108. }
  109. if (lastChild.type.is(termsModule.BlankLine)) {
  110. // HACK: BlankLine can contain any number above 2 of \n's.
  111. // Include every one except for the last one
  112. return { from: content!.from, to: lastChild.to - 1 }
  113. }
  114. return content
  115. },
  116. }),
  117. // disable spell check in these node types when they're inside these parents (empty string = any parent)
  118. noSpellCheckProp.add({
  119. BibKeyArgument: [['']],
  120. BibliographyArgument: [['']],
  121. BibliographyStyleArgument: [['']],
  122. DocumentClassArgument: [['']],
  123. LabelArgument: [['']],
  124. PackageArgument: [['']],
  125. RefArgument: [['']],
  126. OptionalArgument: [
  127. ['DocumentClass'],
  128. ['IncludeGraphics'],
  129. ['LineBreak'],
  130. ['UsePackage'],
  131. ['FigureEnvironment', 'BeginEnv'],
  132. ['ListEnvironment', 'BeginEnv'],
  133. ],
  134. ShortTextArgument: [['Date'], ['SetLengthCommand']],
  135. TextArgument: [['TabularEnvironment', 'BeginEnv']],
  136. }),
  137. // TODO: does this override groups defined in the grammar?
  138. NodeProp.group.add(type => {
  139. const types = []
  140. if (
  141. Tokens.ctrlSeq.includes(type.name) ||
  142. Tokens.ctrlSym.includes(type.name)
  143. ) {
  144. types.push('$CtrlSeq')
  145. if (Tokens.ctrlSym.includes(type.name)) {
  146. types.push('$CtrlSym')
  147. }
  148. } else if (Tokens.envName.includes(type.name)) {
  149. types.push('$EnvName')
  150. } else if (type.name.endsWith('Command')) {
  151. types.push('$Command')
  152. } else if (type.name.endsWith('Argument')) {
  153. types.push('$Argument')
  154. if (
  155. type.name.endsWith('TextArgument') ||
  156. type.is('SectioningArgument')
  157. ) {
  158. types.push('$TextArgument')
  159. }
  160. } else if (type.name.endsWith('Brace')) {
  161. types.push('$Brace')
  162. }
  163. if (type.name in typeMap) {
  164. types.push(...typeMap[type.name])
  165. }
  166. return types.length > 0 ? types : undefined
  167. }),
  168. styleTags({
  169. ...Styles.ctrlSeq,
  170. ...Styles.ctrlSym,
  171. ...Styles.envName,
  172. 'HrefCommand/ShortTextArgument/ShortArg/...': t.link,
  173. 'HrefCommand/UrlArgument/...': t.monospace,
  174. 'CtrlSeq Csname': t.tagName,
  175. 'DocumentClass/OptionalArgument/ShortOptionalArg/...': t.attributeValue,
  176. 'DocumentClass/ShortTextArgument/ShortArg/Normal': t.typeName,
  177. 'ListEnvironment/BeginEnv/OptionalArgument/...': t.monospace,
  178. Number: t.number,
  179. OpenBrace: t.brace,
  180. CloseBrace: t.brace,
  181. OpenBracket: t.squareBracket,
  182. CloseBracket: t.squareBracket,
  183. Dollar: t.string,
  184. Math: t.string,
  185. 'Math/MathChar': t.string,
  186. 'Math/MathSpecialChar': t.string,
  187. 'Math/Number': t.string,
  188. 'MathGroup/OpenBrace MathGroup/CloseBrace': t.string,
  189. 'MathTextCommand/TextArgument/OpenBrace MathTextCommand/TextArgument/CloseBrace':
  190. t.string,
  191. 'MathOpening/LeftCtrlSeq MathClosing/RightCtrlSeq MathUnknownCommand/CtrlSeq MathTextCommand/CtrlSeq':
  192. t.literal,
  193. MathDelimiter: t.literal,
  194. DoubleDollar: t.keyword,
  195. Tilde: t.keyword,
  196. Ampersand: t.keyword,
  197. LineBreakCtrlSym: t.keyword,
  198. Comment: t.comment,
  199. 'UsePackage/OptionalArgument/ShortOptionalArg/Normal': t.attributeValue,
  200. 'UsePackage/ShortTextArgument/ShortArg/Normal': t.tagName,
  201. 'Affiliation/OptionalArgument/ShortOptionalArg/Normal':
  202. t.attributeValue,
  203. 'Affil/OptionalArgument/ShortOptionalArg/Normal': t.attributeValue,
  204. 'LiteralArgContent VerbContent VerbatimContent LstInlineContent':
  205. t.string,
  206. 'NewCommand/LiteralArgContent': t.typeName,
  207. 'LabelArgument/ShortTextArgument/ShortArg/...': t.attributeValue,
  208. 'RefArgument/ShortTextArgument/ShortArg/...': t.attributeValue,
  209. 'BibKeyArgument/ShortTextArgument/ShortArg/...': t.attributeValue,
  210. 'ShortTextArgument/ShortArg/Normal': t.monospace,
  211. 'UrlArgument/LiteralArgContent': [t.attributeValue, t.url],
  212. 'FilePathArgument/LiteralArgContent': t.attributeValue,
  213. 'BareFilePathArgument/SpaceDelimitedLiteralArgContent':
  214. t.attributeValue,
  215. TrailingContent: t.comment,
  216. 'Item/OptionalArgument/ShortOptionalArg/...': t.strong,
  217. // TODO: t.strong, t.emphasis
  218. }),
  219. ],
  220. }),
  221. languageData: {
  222. commentTokens: { line: '%' },
  223. closeBrackets: closeBracketConfig,
  224. },
  225. })