visual-theme.ts 14 KB

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