latex-language.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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. Subfile: ['$CommandTooltipCommand'],
  51. Ref: ['$CommandTooltipCommand'],
  52. UrlCommand: ['$CommandTooltipCommand'],
  53. // text formatting commands that can be toggled via the toolbar
  54. TextBoldCommand: ['$ToggleTextFormattingCommand'],
  55. TextItalicCommand: ['$ToggleTextFormattingCommand'],
  56. // text formatting commands that cannot be toggled via the toolbar
  57. TextSmallCapsCommand: ['$OtherTextFormattingCommand'],
  58. TextTeletypeCommand: ['$OtherTextFormattingCommand'],
  59. TextMediumCommand: ['$OtherTextFormattingCommand'],
  60. TextSansSerifCommand: ['$OtherTextFormattingCommand'],
  61. TextSuperscriptCommand: ['$OtherTextFormattingCommand'],
  62. TextSubscriptCommand: ['$OtherTextFormattingCommand'],
  63. StrikeOutCommand: ['$OtherTextFormattingCommand'],
  64. EmphasisCommand: ['$OtherTextFormattingCommand'],
  65. UnderlineCommand: ['$OtherTextFormattingCommand'],
  66. }
  67. export const LaTeXLanguage = LRLanguage.define({
  68. name: 'latex',
  69. parser: parser.configure({
  70. props: [
  71. foldNodeProp.add({
  72. Comment: (node, state) => {
  73. if (commentIsOpenFold(node, state)) {
  74. const closingFoldNode = findClosingFoldComment(node, state)
  75. if (closingFoldNode) {
  76. return getFoldRange(node, closingFoldNode, state)
  77. }
  78. }
  79. return null
  80. },
  81. Group: foldInside,
  82. NonEmptyGroup: foldInside,
  83. TextArgument: foldInside,
  84. // TODO: Why isn't
  85. // `Content: node => node,`
  86. // enough? For some reason it doesn't work if there's a newline after
  87. // \section{a}, but works for \section{a}b
  88. $Environment: node => node.getChild('Content'),
  89. $Section: node => {
  90. const BACKWARDS = -1
  91. const lastChild = node.resolveInner(node.to, BACKWARDS)
  92. const content = node.getChild('Content')
  93. if (!content) {
  94. return null
  95. }
  96. if (lastChild.type.is(termsModule.NewLine)) {
  97. // Ignore last newline for sectioning commands
  98. return { from: content!.from, to: lastChild.from }
  99. }
  100. if (lastChild.type.is(termsModule.Whitespace)) {
  101. // If the sectioningcommand is indented on a newline
  102. let sibling = lastChild.prevSibling
  103. while (sibling?.type.is(termsModule.Whitespace)) {
  104. sibling = sibling.prevSibling
  105. }
  106. if (sibling?.type.is(termsModule.NewLine)) {
  107. return { from: content!.from, to: sibling.from }
  108. }
  109. if (sibling?.type.is(termsModule.Comment)) {
  110. // A trailing comment line (e.g. `%`) before an indented
  111. // sectioning command should be included in the fold, but the
  112. // sectioning command itself should still appear on its own
  113. // line. The Comment node consumes its trailing newline, so
  114. // end the fold one position before the Comment ends.
  115. return { from: content!.from, to: sibling.to - 1 }
  116. }
  117. if (sibling?.type.is(termsModule.BlankLine)) {
  118. // Blank line(s) between previous content and an indented
  119. // sectioning command: include all but the last newline in the
  120. // fold (mirroring the BlankLine handling below).
  121. return { from: content!.from, to: sibling.to - 1 }
  122. }
  123. }
  124. if (lastChild.type.is(termsModule.BlankLine)) {
  125. // HACK: BlankLine can contain any number above 2 of \n's.
  126. // Include every one except for the last one
  127. return { from: content!.from, to: lastChild.to - 1 }
  128. }
  129. return content
  130. },
  131. }),
  132. // disable spell check in these node types when they're inside these parents (empty string = any parent)
  133. noSpellCheckProp.add({
  134. BibKeyArgument: [['']],
  135. BibliographyArgument: [['']],
  136. BibliographyStyleArgument: [['']],
  137. DocumentClassArgument: [['']],
  138. LabelArgument: [['']],
  139. PackageArgument: [['']],
  140. RefArgument: [['']],
  141. OptionalArgument: [
  142. ['DocumentClass'],
  143. ['IncludeGraphics'],
  144. ['LineBreak'],
  145. ['UsePackage'],
  146. ['FigureEnvironment', 'BeginEnv'],
  147. ['ListEnvironment', 'BeginEnv'],
  148. ],
  149. ShortTextArgument: [['Date'], ['SetLengthCommand']],
  150. TextArgument: [['TabularEnvironment', 'BeginEnv']],
  151. }),
  152. // TODO: does this override groups defined in the grammar?
  153. NodeProp.group.add(type => {
  154. const types = []
  155. if (
  156. Tokens.ctrlSeq.includes(type.name) ||
  157. Tokens.ctrlSym.includes(type.name)
  158. ) {
  159. types.push('$CtrlSeq')
  160. if (Tokens.ctrlSym.includes(type.name)) {
  161. types.push('$CtrlSym')
  162. }
  163. } else if (Tokens.envName.includes(type.name)) {
  164. types.push('$EnvName')
  165. } else if (type.name.endsWith('Command')) {
  166. types.push('$Command')
  167. } else if (type.name.endsWith('Argument')) {
  168. types.push('$Argument')
  169. if (
  170. type.name.endsWith('TextArgument') ||
  171. type.is('SectioningArgument')
  172. ) {
  173. types.push('$TextArgument')
  174. }
  175. } else if (type.name.endsWith('Brace')) {
  176. types.push('$Brace')
  177. }
  178. if (type.name in typeMap) {
  179. types.push(...typeMap[type.name])
  180. }
  181. return types.length > 0 ? types : undefined
  182. }),
  183. styleTags({
  184. ...Styles.ctrlSeq,
  185. ...Styles.ctrlSym,
  186. ...Styles.envName,
  187. 'HrefCommand/ShortTextArgument/ShortArg/...': t.link,
  188. 'HrefCommand/UrlArgument/...': t.monospace,
  189. 'CtrlSeq Csname': t.tagName,
  190. 'DocumentClass/OptionalArgument/ShortOptionalArg/...': t.attributeValue,
  191. 'DocumentClass/ShortTextArgument/ShortArg/Normal': t.typeName,
  192. 'ListEnvironment/BeginEnv/OptionalArgument/...': t.monospace,
  193. Number: t.number,
  194. OpenBrace: t.brace,
  195. CloseBrace: t.brace,
  196. OpenBracket: t.squareBracket,
  197. CloseBracket: t.squareBracket,
  198. Dollar: t.string,
  199. Math: t.string,
  200. 'Math/MathChar': t.string,
  201. 'Math/MathSpecialChar': t.string,
  202. 'Math/Number': t.string,
  203. 'MathGroup/OpenBrace MathGroup/CloseBrace': t.string,
  204. 'MathTextCommand/TextArgument/OpenBrace MathTextCommand/TextArgument/CloseBrace':
  205. t.string,
  206. 'MathOpening/LeftCtrlSeq MathClosing/RightCtrlSeq MathUnknownCommand/CtrlSeq MathTextCommand/CtrlSeq':
  207. t.literal,
  208. MathDelimiter: t.literal,
  209. DoubleDollar: t.keyword,
  210. Tilde: t.keyword,
  211. Ampersand: t.keyword,
  212. LineBreakCtrlSym: t.keyword,
  213. Comment: t.comment,
  214. 'UsePackage/OptionalArgument/ShortOptionalArg/Normal': t.attributeValue,
  215. 'UsePackage/ShortTextArgument/ShortArg/Normal': t.tagName,
  216. 'Affiliation/OptionalArgument/ShortOptionalArg/Normal':
  217. t.attributeValue,
  218. 'Affil/OptionalArgument/ShortOptionalArg/Normal': t.attributeValue,
  219. 'LiteralArgContent VerbContent VerbatimContent LstInlineContent':
  220. t.string,
  221. 'NewCommand/LiteralArgContent': t.typeName,
  222. 'LabelArgument/ShortTextArgument/ShortArg/...': t.attributeValue,
  223. 'RefArgument/ShortTextArgument/ShortArg/...': t.attributeValue,
  224. 'BibKeyArgument/ShortTextArgument/ShortArg/...': t.attributeValue,
  225. 'ShortTextArgument/ShortArg/Normal': t.monospace,
  226. 'UrlArgument/LiteralArgContent': [t.attributeValue, t.url],
  227. 'FilePathArgument/LiteralArgContent': t.attributeValue,
  228. 'BareFilePathArgument/SpaceDelimitedLiteralArgContent':
  229. t.attributeValue,
  230. TrailingContent: t.comment,
  231. 'Item/OptionalArgument/ShortOptionalArg/...': t.strong,
  232. // TODO: t.strong, t.emphasis
  233. }),
  234. ],
  235. }),
  236. languageData: {
  237. commentTokens: { line: '%' },
  238. closeBrackets: closeBracketConfig,
  239. },
  240. })