latex-language.ts 8.2 KB

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