visual-theme.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. import { EditorView } from '@codemirror/view'
  2. import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
  3. import { tags } from '@lezer/highlight'
  4. import { Annotation, Compartment, Extension, Facet } from '@codemirror/state'
  5. /**
  6. * A syntax highlighter for content types that are only styled in the visual editor.
  7. */
  8. export const visualHighlightStyle = syntaxHighlighting(
  9. HighlightStyle.define([
  10. { tag: tags.link, class: 'ol-cm-link-text' },
  11. { tag: tags.url, class: 'ol-cm-url' },
  12. { tag: tags.typeName, class: 'ol-cm-monospace' },
  13. { tag: tags.attributeValue, class: 'ol-cm-monospace' },
  14. { tag: tags.keyword, class: 'ol-cm-monospace' },
  15. { tag: tags.string, class: 'ol-cm-monospace' },
  16. { tag: tags.punctuation, class: 'ol-cm-punctuation' },
  17. { tag: tags.literal, class: 'ol-cm-monospace' },
  18. { tag: tags.strong, class: 'ol-cm-strong' },
  19. {
  20. tag: tags.monospace,
  21. fontFamily: 'var(--source-font-family)',
  22. lineHeight: 1,
  23. overflowWrap: 'break-word',
  24. },
  25. ])
  26. )
  27. const mainVisualTheme = EditorView.theme({
  28. '&.cm-editor': {
  29. '--visual-font-family':
  30. "'Noto Serif', 'Palatino Linotype', 'Book Antiqua', Palatino, serif !important",
  31. '--visual-font-size': 'calc(var(--font-size) * 1.15)',
  32. '& .cm-content': {
  33. opacity: 0,
  34. },
  35. '&.ol-cm-parsed .cm-content': {
  36. opacity: 1,
  37. transition: 'opacity 0.1s ease-out',
  38. },
  39. },
  40. '.cm-content.cm-content': {
  41. overflowX: 'hidden', // needed so the callout elements don't overflow (requires line wrapping to be on)
  42. padding:
  43. '0 max(calc((var(--content-width) - 95ch) / 2), calc(var(--content-width) * 0.08))', // max 95 characters per line
  44. fontFamily: 'var(--visual-font-family)',
  45. fontSize: 'var(--visual-font-size)',
  46. },
  47. '.cm-cursor-primary.cm-cursor-primary': {
  48. fontFamily: 'var(--visual-font-family)',
  49. fontSize: 'var(--visual-font-size)',
  50. },
  51. '.cm-line': {
  52. overflowX: 'visible', // needed so the callout elements can overflow when the content has padding
  53. },
  54. '.cm-gutter': {
  55. opacity: '0.5',
  56. },
  57. '.cm-tooltip': {
  58. fontSize: 'calc(var(--font-size) * 1.15) !important',
  59. },
  60. '.ol-cm-link-text': {
  61. textDecoration: 'underline',
  62. fontFamily: 'inherit',
  63. textUnderlineOffset: '2px',
  64. },
  65. '.ol-cm-monospace': {
  66. fontFamily: 'var(--source-font-family)',
  67. lineHeight: 1,
  68. fontWeight: 'normal',
  69. fontStyle: 'normal',
  70. fontVariant: 'normal',
  71. textDecoration: 'none',
  72. },
  73. '.ol-cm-strong': {
  74. fontWeight: 700,
  75. },
  76. '.ol-cm-punctuation': {
  77. fontFamily: 'var(--source-font-family)',
  78. lineHeight: 1,
  79. },
  80. '.ol-cm-brace': {
  81. opacity: '0.5',
  82. },
  83. '.ol-cm-math': {
  84. overflow: 'hidden', // stop the margin from the inner math element affecting the block height
  85. },
  86. '.ol-cm-maketitle': {
  87. textAlign: 'center',
  88. paddingBottom: '2em',
  89. },
  90. '.ol-cm-title': {
  91. fontSize: '1.7em',
  92. cursor: 'pointer',
  93. padding: '0.5em',
  94. lineHeight: 'calc(var(--line-height) * 5/6)',
  95. textWrap: 'balance',
  96. },
  97. '.ol-cm-authors': {
  98. display: 'flex',
  99. justifyContent: 'space-evenly',
  100. gap: '0.5em',
  101. flexWrap: 'wrap',
  102. },
  103. '.ol-cm-author': {
  104. cursor: 'pointer',
  105. display: 'inline-block',
  106. minWidth: '150px',
  107. },
  108. '.ol-cm-icon-brace': {
  109. filter: 'grayscale(1)',
  110. marginRight: '2px',
  111. },
  112. '.ol-cm-indicator': {
  113. color: 'rgba(125, 125, 125, 0.5)',
  114. },
  115. '.ol-cm-begin': {
  116. fontFamily: 'var(--source-font-family)',
  117. minHeight: '1em',
  118. textAlign: 'center',
  119. display: 'flex',
  120. alignItems: 'center',
  121. justifyContent: 'center',
  122. },
  123. '.ol-cm-end': {
  124. fontFamily: 'var(--source-font-family)',
  125. paddingBottom: '1.5em',
  126. minHeight: '1em',
  127. textAlign: 'center',
  128. justifyContent: 'center',
  129. background: `linear-gradient(180deg, rgba(0,0,0,0) calc(50% - 1px), rgba(192,192,192,1) calc(50%), rgba(0,0,0,0) calc(50% + 1px))`,
  130. },
  131. '.ol-cm-environment-top': {
  132. paddingTop: '1em',
  133. },
  134. '.ol-cm-environment-bottom': {
  135. paddingBottom: '1em',
  136. },
  137. '.ol-cm-environment-first-line': {
  138. paddingTop: '0.5em !important',
  139. borderTopLeftRadius: '8px',
  140. borderTopRightRadius: '8px',
  141. },
  142. '.ol-cm-environment-last-line': {
  143. paddingBottom: '1em !important',
  144. borderBottomLeftRadius: '8px',
  145. borderBottomRightRadius: '8px',
  146. },
  147. '.ol-cm-environment-figure.ol-cm-environment-line, .ol-cm-environment-table.ol-cm-environment-line':
  148. {
  149. backgroundColor: 'rgba(125, 125, 125, 0.05)',
  150. padding: '0 12px',
  151. },
  152. '.ol-cm-environment-figure.ol-cm-environment-last-line, .ol-cm-environment-table.ol-cm-environment-last-line, .ol-cm-preamble-line.ol-cm-environment-last-line':
  153. {
  154. boxShadow: '0 2px 5px -3px rgb(125, 125, 125, 0.5)',
  155. },
  156. '.ol-cm-environment-theorem-plain': {
  157. fontStyle: 'italic',
  158. },
  159. '.ol-cm-begin-proof > .ol-cm-environment-name': {
  160. fontStyle: 'italic',
  161. },
  162. '.ol-cm-environment-quote-block.ol-cm-environment-line': {
  163. borderLeft: '4px solid rgba(125, 125, 125, 0.25)',
  164. paddingLeft: '1em',
  165. borderRadius: '0',
  166. },
  167. '.ol-cm-environment-padding': {
  168. flex: 1,
  169. height: '1px',
  170. background: `linear-gradient(180deg, rgba(0,0,0,0) calc(50% - 1px), rgba(192,192,192,1) calc(50%), rgba(0,0,0,0) calc(50% + 1px))`,
  171. },
  172. '.ol-cm-environment-name': {
  173. padding: '0 1em',
  174. },
  175. '.ol-cm-begin-abstract > .ol-cm-environment-name': {
  176. fontFamily: 'var(--visual-font-family)',
  177. fontSize: '1.2em',
  178. fontWeight: 550,
  179. textTransform: 'capitalize',
  180. },
  181. '.ol-cm-begin-theorem > .ol-cm-environment-name': {
  182. fontFamily: 'var(--visual-font-family)',
  183. fontWeight: 550,
  184. padding: '0 6px',
  185. textTransform: 'capitalize',
  186. },
  187. '.ol-cm-begin-theorem > .ol-cm-environment-padding:first-of-type': {
  188. flex: 0,
  189. },
  190. '.ol-cm-item, .ol-cm-description-item': {
  191. paddingInlineStart: 'calc(var(--list-depth) * 2ch)',
  192. },
  193. '.ol-cm-item::before': {
  194. counterReset: 'list-item var(--list-ordinal)',
  195. content: 'counter(list-item, var(--list-type)) var(--list-suffix)',
  196. },
  197. '.ol-cm-heading': {
  198. fontWeight: 550,
  199. lineHeight: '1.35',
  200. color: 'inherit !important',
  201. background: 'inherit !important',
  202. },
  203. '.ol-cm-command-part': {
  204. fontSize: '2em',
  205. },
  206. '.ol-cm-command-chapter': {
  207. fontSize: '1.6em',
  208. },
  209. '.ol-cm-command-section': {
  210. fontSize: '1.44em',
  211. },
  212. '.ol-cm-command-subsection': {
  213. fontSize: '1.2em',
  214. },
  215. '.ol-cm-command-subsubsection': {
  216. fontSize: '1em',
  217. },
  218. '.ol-cm-command-paragraph': {
  219. fontSize: '1em',
  220. },
  221. '.ol-cm-command-subparagraph': {
  222. fontSize: '1em',
  223. },
  224. '.ol-cm-frame-title': {
  225. fontSize: '1.44em',
  226. },
  227. '.ol-cm-frame-subtitle': {
  228. fontSize: '1em',
  229. },
  230. '.ol-cm-divider': {
  231. borderBottom: '1px solid rgba(125, 125, 125, 0.1)',
  232. padding: '0.5em 6px',
  233. '&.ol-cm-frame-widget': {
  234. borderBottom: 'none',
  235. borderTop: '1px solid rgba(125, 125, 125, 0.1)',
  236. },
  237. },
  238. '.ol-cm-command-textbf': {
  239. fontWeight: 700,
  240. },
  241. '.ol-cm-command-textit': {
  242. fontStyle: 'italic',
  243. },
  244. '.ol-cm-command-textsc': {
  245. fontVariant: 'small-caps',
  246. },
  247. '.ol-cm-command-texttt': {
  248. fontFamily: 'monospace',
  249. },
  250. '.ol-cm-command-textmd, .ol-cm-command-textmd > .ol-cm-command-textbf': {
  251. fontWeight: 'normal',
  252. },
  253. '.ol-cm-command-textsf': {
  254. fontFamily: 'var(--source-font-family)',
  255. },
  256. '.ol-cm-command-textsuperscript': {
  257. verticalAlign: 'super',
  258. fontSize: 'smaller',
  259. lineHeight: 'calc(var(--line-height) / 2)',
  260. },
  261. '.ol-cm-command-textsubscript': {
  262. verticalAlign: 'sub',
  263. fontSize: 'smaller',
  264. lineHeight: 'calc(var(--line-height) / 2)',
  265. },
  266. '.ol-cm-command-underline': {
  267. textDecoration: 'underline',
  268. },
  269. '.ol-cm-command-sout': {
  270. textDecoration: 'line-through',
  271. },
  272. '.ol-cm-command-emph': {
  273. fontStyle: 'italic',
  274. '& .ol-cm-command-textit': {
  275. fontStyle: 'normal',
  276. },
  277. '.ol-cm-command-textit &': {
  278. fontStyle: 'normal',
  279. },
  280. },
  281. '.ol-cm-command-url': {
  282. textDecoration: 'underline',
  283. // copied from tags.monospace
  284. fontFamily: 'var(--source-font-family)',
  285. lineHeight: 1,
  286. overflowWrap: 'break-word',
  287. hyphens: 'auto',
  288. },
  289. '.ol-cm-space': {
  290. display: 'inline-block',
  291. },
  292. '.ol-cm-environment-centered': {
  293. '&.ol-cm-label-line, &.ol-cm-caption-line': {
  294. textAlign: 'center',
  295. },
  296. '&.ol-cm-caption-line': {
  297. padding: '0 10%',
  298. },
  299. },
  300. '.ol-cm-caption-line .ol-cm-label': {
  301. marginRight: '1ch',
  302. },
  303. '.ol-cm-environment-verbatim': {
  304. fontFamily: 'var(--source-font-family)',
  305. },
  306. '.ol-cm-environment-lstlisting': {
  307. fontFamily: 'var(--source-font-family)',
  308. },
  309. '.ol-cm-tex': {
  310. textTransform: 'uppercase',
  311. '& sup': {
  312. position: 'inherit',
  313. fontSize: '0.85em',
  314. verticalAlign: '0.15em',
  315. marginLeft: '-0.36em',
  316. marginRight: '-0.15em',
  317. },
  318. '& sub': {
  319. position: 'inherit',
  320. fontSize: '1em',
  321. verticalAlign: '-0.5ex',
  322. marginLeft: '-0.1667em',
  323. marginRight: '-0.125em',
  324. },
  325. },
  326. '.ol-cm-graphics': {
  327. display: 'block',
  328. maxWidth: 'min(300px, 100%)',
  329. paddingTop: '1em',
  330. paddingBottom: '1em',
  331. cursor: 'pointer',
  332. '.ol-cm-graphics-inline &': {
  333. display: 'inline',
  334. },
  335. },
  336. '.ol-cm-graphics-inline-edit-wrapper': {
  337. display: 'inline-block',
  338. position: 'relative',
  339. verticalAlign: 'middle',
  340. '& .ol-cm-graphics': {
  341. paddingTop: 0,
  342. paddingBottom: 0,
  343. },
  344. },
  345. '.ol-cm-graphics-loading': {
  346. height: '300px', // guess that the height is the same as the max width
  347. },
  348. '.ol-cm-graphics-error': {
  349. border: '1px solid red',
  350. padding: '8px',
  351. },
  352. '.ol-cm-environment-centered .ol-cm-graphics': {
  353. margin: '0 auto',
  354. },
  355. '.ol-cm-command-verb .ol-cm-monospace': {
  356. color: 'inherit', // remove syntax highlighting colour from verbatim content
  357. },
  358. '.ol-cm-preamble-wrapper': {
  359. padding: '0.5em 0',
  360. '&.ol-cm-preamble-expanded': {
  361. paddingBottom: '0',
  362. },
  363. },
  364. '.ol-cm-preamble-widget, .ol-cm-end-document-widget': {
  365. padding: '0.25em 1em',
  366. borderRadius: '8px',
  367. fontFamily: 'var(--font-sans)',
  368. fontSize: '14px',
  369. '.ol-cm-preamble-expanded &': {
  370. borderBottomLeftRadius: '0',
  371. borderBottomRightRadius: '0',
  372. borderBottom: '1px solid rgba(125, 125, 125, 0.2)',
  373. },
  374. },
  375. '.ol-cm-preamble-widget': {
  376. cursor: 'pointer',
  377. display: 'flex',
  378. justifyContent: 'space-between',
  379. alignItems: 'center',
  380. },
  381. '.ol-cm-preamble-expand-icon': {
  382. width: '32px',
  383. lineHeight: '32px',
  384. textAlign: 'center',
  385. transition: '0.2s ease-out',
  386. opacity: '0.5',
  387. '.ol-cm-preamble-widget:hover &': {
  388. opacity: '1',
  389. },
  390. '.ol-cm-preamble-expanded &': {
  391. transform: 'rotate(180deg)',
  392. },
  393. },
  394. '.ol-cm-preamble-line, .ol-cm-end-document-widget, .ol-cm-preamble-widget': {
  395. backgroundColor: 'rgba(125, 125, 125, 0.05)',
  396. },
  397. '.ol-cm-preamble-line': {
  398. padding: '0 12px',
  399. '&.ol-cm-environment-first-line': {
  400. borderRadius: '0',
  401. },
  402. },
  403. '.ol-cm-end-document-widget': {
  404. textAlign: 'center',
  405. },
  406. '.ol-cm-environment-figure': {
  407. position: 'relative',
  408. },
  409. '.ol-cm-graphics-edit-button': {
  410. position: 'absolute',
  411. top: '18px',
  412. right: '18px',
  413. },
  414. '.ol-cm-footnote': {
  415. display: 'inline-flex',
  416. padding: '0 0.1em',
  417. background: 'rgba(125, 125, 125, 0.25)',
  418. borderRadius: '2px',
  419. height: '1em',
  420. cursor: 'pointer',
  421. verticalAlign: 'text-top',
  422. '&:not(.ol-cm-footnote-view):hover': {
  423. background: 'rgba(125, 125, 125, 0.5)',
  424. },
  425. '&.ol-cm-footnote-view': {
  426. height: 'auto',
  427. verticalAlign: 'unset',
  428. display: 'inline',
  429. padding: '0 0.5em',
  430. },
  431. },
  432. })
  433. const contentWidthThemeConf = new Compartment()
  434. const changeContentWidthAnnotation = Annotation.define<boolean>()
  435. const currentWidth = Facet.define<string, string>({
  436. combine: values => {
  437. if (values.length === 0) {
  438. return ''
  439. }
  440. return values[0]
  441. },
  442. })
  443. function createContentWidthTheme(contentWidth: string) {
  444. return [
  445. currentWidth.of(contentWidth),
  446. EditorView.editorAttributes.of({
  447. style: `--content-width: ${contentWidth}`,
  448. }),
  449. ]
  450. }
  451. const contentWidthSetter = EditorView.updateListener.of(update => {
  452. if (update.geometryChanged && !update.docChanged) {
  453. // Ignore any update triggered by this plugin
  454. if (
  455. update.transactions.some(tr =>
  456. tr.annotation(changeContentWidthAnnotation)
  457. )
  458. ) {
  459. return
  460. }
  461. const viewWidth = `${update.view.contentDOM.offsetWidth}px`
  462. const currentConfiguredWidth = update.state.facet(currentWidth)
  463. if (currentConfiguredWidth === viewWidth) {
  464. // We already have the correct width stored
  465. return
  466. }
  467. update.view.dispatch({
  468. effects: contentWidthThemeConf.reconfigure(
  469. createContentWidthTheme(viewWidth)
  470. ),
  471. // Set the selection explicitly to force the cursor to redraw if CM6
  472. // fails to spot a change in geometry, which sometimes seems to happen
  473. // (see #15145)
  474. selection: update.view.state.selection,
  475. annotations: changeContentWidthAnnotation.of(true),
  476. })
  477. }
  478. })
  479. export const visualTheme: Extension = [
  480. contentWidthThemeConf.of(createContentWidthTheme('100%')),
  481. mainVisualTheme,
  482. contentWidthSetter,
  483. ]