latex-language.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. }
  52. export const LaTeXLanguage = LRLanguage.define({
  53. name: 'latex',
  54. parser: parser.configure({
  55. props: [
  56. foldNodeProp.add({
  57. Comment: (node, state) => {
  58. if (commentIsOpenFold(node, state)) {
  59. const closingFoldNode = findClosingFoldComment(node, state)
  60. if (closingFoldNode) {
  61. return getFoldRange(node, closingFoldNode, state)
  62. }
  63. }
  64. return null
  65. },
  66. Group: foldInside,
  67. NonEmptyGroup: foldInside,
  68. TextArgument: foldInside,
  69. // TODO: Why isn't
  70. // `Content: node => node,`
  71. // enough? For some reason it doesn't work if there's a newline after
  72. // \section{a}, but works for \section{a}b
  73. $Environment: node => node.getChild('Content'),
  74. $SectioningCommand: node => {
  75. const BACKWARDS = -1
  76. const lastChild = node.resolveInner(node.to, BACKWARDS)
  77. const content = node.getChild('Content')
  78. if (!content) {
  79. return null
  80. }
  81. if (lastChild.type.is(termsModule.NewLine)) {
  82. // Ignore last newline for sectioning commands
  83. return { from: content!.from, to: lastChild.from }
  84. }
  85. if (lastChild.type.is(termsModule.Whitespace)) {
  86. // If the sectioningcommand is indented on a newline
  87. let sibling = lastChild.prevSibling
  88. while (sibling?.type.is(termsModule.Whitespace)) {
  89. sibling = sibling.prevSibling
  90. }
  91. if (sibling?.type.is(termsModule.NewLine)) {
  92. return { from: content!.from, to: sibling.from }
  93. }
  94. }
  95. if (lastChild.type.is(termsModule.BlankLine)) {
  96. // HACK: BlankLine can contain any number above 2 of \n's.
  97. // Include every one except for the last one
  98. return { from: content!.from, to: lastChild.to - 1 }
  99. }
  100. return content
  101. },
  102. }),
  103. // TODO: does this override groups defined in the grammar?
  104. NodeProp.group.add(type => {
  105. const types = []
  106. if (
  107. Tokens.ctrlSeq.includes(type.name) ||
  108. Tokens.ctrlSym.includes(type.name)
  109. ) {
  110. types.push('$CtrlSeq')
  111. if (Tokens.ctrlSym.includes(type.name)) {
  112. types.push('$CtrlSym')
  113. }
  114. } else if (Tokens.envName.includes(type.name)) {
  115. types.push('$EnvName')
  116. } else if (type.name.endsWith('Command')) {
  117. types.push('$Command')
  118. } else if (type.name.endsWith('Argument')) {
  119. types.push('$Argument')
  120. if (
  121. type.name.endsWith('TextArgument') ||
  122. type.is('SectioningArgument')
  123. ) {
  124. types.push('$TextArgument')
  125. }
  126. } else if (type.name.endsWith('Environment')) {
  127. types.push('$Environment')
  128. } else if (type.name.endsWith('Brace')) {
  129. types.push('$Brace')
  130. } else if (
  131. ['BracketMath', 'ParenMath', 'DollarMath'].includes(type.name)
  132. ) {
  133. types.push('$MathContainer')
  134. }
  135. if (type.name in typeMap) {
  136. types.push(...typeMap[type.name])
  137. }
  138. return types.length > 0 ? types : undefined
  139. }),
  140. styleTags({
  141. ...Styles.ctrlSeq,
  142. ...Styles.ctrlSym,
  143. ...Styles.envName,
  144. 'HrefCommand/ShortTextArgument/ShortArg/...': t.link,
  145. 'HrefCommand/UrlArgument/...': t.monospace,
  146. 'CtrlSeq Csname': t.tagName,
  147. 'DocumentClass/OptionalArgument/ShortOptionalArg/Normal':
  148. t.attributeValue,
  149. 'DocumentClass/ShortTextArgument/ShortArg/Normal': t.typeName,
  150. Number: t.number,
  151. OpenBrace: t.brace,
  152. CloseBrace: t.brace,
  153. OpenBracket: t.squareBracket,
  154. CloseBracket: t.squareBracket,
  155. Dollar: t.string,
  156. Math: t.string,
  157. 'Math/MathChar': t.string,
  158. 'Math/MathSpecialChar': t.string,
  159. 'Math/Number': t.string,
  160. 'MathGroup/OpenBrace MathGroup/CloseBrace': t.string,
  161. 'MathTextCommand/TextArgument/OpenBrace MathTextCommand/TextArgument/CloseBrace':
  162. t.string,
  163. 'MathOpening/LeftCtrlSeq MathClosing/RightCtrlSeq MathCommand/CtrlSeq MathTextCommand/CtrlSeq':
  164. t.literal,
  165. MathDelimiter: t.literal,
  166. DoubleDollar: t.keyword,
  167. Tilde: t.keyword,
  168. Ampersand: t.keyword,
  169. LineBreakCtrlSym: t.keyword,
  170. Comment: t.comment,
  171. 'UsePackage/OptionalArgument/ShortOptionalArg/Normal': t.attributeValue,
  172. 'UsePackage/ShortTextArgument/ShortArg/Normal': t.tagName,
  173. 'LiteralArgContent VerbContent VerbatimContent LstInlineContent':
  174. t.string,
  175. 'NewCommand/LiteralArgContent': t.typeName,
  176. 'LabelArgument/ShortTextArgument/ShortArg/...': t.attributeValue,
  177. 'RefArgument/ShortTextArgument/ShortArg/...': t.attributeValue,
  178. 'BibKeyArgument/ShortTextArgument/ShortArg/...': t.attributeValue,
  179. 'ShortTextArgument/ShortArg/Normal': t.monospace,
  180. 'UrlArgument/LiteralArgContent': [t.attributeValue, t.url],
  181. 'FilePathArgument/LiteralArgContent': t.attributeValue,
  182. 'BareFilePathArgument/SpaceDelimitedLiteralArgContent':
  183. t.attributeValue,
  184. TrailingContent: t.comment,
  185. // TODO: t.strong, t.emphasis
  186. }),
  187. ],
  188. }),
  189. languageData: {
  190. commentTokens: { line: '%' },
  191. closeBrackets: closeBracketConfig,
  192. },
  193. })