visual-theme.ts 13 KB

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