usePrismTokens.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import {useCallback, useEffect, useMemo, useState} from 'react';
  2. import * as Sentry from '@sentry/react';
  3. import Prism from 'prismjs';
  4. import {trackAnalytics} from 'sentry/utils/analytics';
  5. import {loadPrismLanguage, prismLanguageMap} from 'sentry/utils/prism';
  6. import useOrganization from 'sentry/utils/useOrganization';
  7. type PrismHighlightParams = {
  8. code: string;
  9. language: string;
  10. };
  11. export type SyntaxHighlightToken = {
  12. children: string;
  13. className: string;
  14. };
  15. export type SyntaxHighlightLine = SyntaxHighlightToken[];
  16. type IntermediateToken = {
  17. children: string;
  18. types: Set<string>;
  19. };
  20. const useLoadPrismLanguage = (language: string, {onLoad}: {onLoad: () => void}) => {
  21. const organization = useOrganization({allowNull: true});
  22. useEffect(() => {
  23. if (!language) {
  24. return;
  25. }
  26. if (!prismLanguageMap[language.toLowerCase()]) {
  27. trackAnalytics('stack_trace.prism_missing_language', {
  28. organization,
  29. attempted_language: language.toLowerCase(),
  30. });
  31. return;
  32. }
  33. loadPrismLanguage(language, {onLoad});
  34. }, [language, onLoad, organization]);
  35. };
  36. const getPrismGrammar = (language: string) => {
  37. try {
  38. const fullLanguage = prismLanguageMap[language];
  39. return Prism.languages[fullLanguage] ?? null;
  40. } catch (e) {
  41. Sentry.captureException(e);
  42. return null;
  43. }
  44. };
  45. const splitMultipleTokensByLine = (
  46. tokens: Array<string | Prism.Token>,
  47. types: Set<string> = new Set(['token'])
  48. ) => {
  49. const lines: IntermediateToken[][] = [];
  50. let currentLine: IntermediateToken[] = [];
  51. for (const token of tokens) {
  52. const tokenLines = splitTokenContentByLine(token, new Set(types));
  53. if (tokenLines.length === 0) {
  54. continue;
  55. }
  56. currentLine.push(...tokenLines[0]);
  57. if (tokenLines.length > 1) {
  58. for (let i = 1; i < tokenLines.length; i++) {
  59. lines.push(currentLine);
  60. currentLine = tokenLines[i];
  61. }
  62. }
  63. }
  64. if (currentLine.length > 0) {
  65. lines.push(currentLine);
  66. }
  67. return lines;
  68. };
  69. // Splits a token by newlines encounted inside of its content.
  70. // Returns an array of lines. If the returned array only has a single
  71. // line, no newlines were found.
  72. const splitTokenContentByLine = (
  73. token: string | Prism.Token,
  74. types: Set<string> = new Set(['token'])
  75. ): IntermediateToken[][] => {
  76. if (typeof token === 'string') {
  77. const lines: IntermediateToken[][] = [];
  78. token.split(/\r?\n/).forEach(line => {
  79. if (line) {
  80. lines.push([{types: new Set(types), children: line}]);
  81. } else {
  82. // If empty string, new line was at the end of the token
  83. lines.push([]);
  84. }
  85. });
  86. return lines;
  87. }
  88. types.add(token.type);
  89. if (Array.isArray(token.content)) {
  90. return splitMultipleTokensByLine(token.content, new Set(types));
  91. }
  92. return splitTokenContentByLine(token.content, types);
  93. };
  94. const breakTokensByLine = (
  95. tokens: Array<string | Prism.Token>
  96. ): SyntaxHighlightLine[] => {
  97. const lines = splitMultipleTokensByLine(tokens);
  98. return lines.map(line =>
  99. line.map(token => ({
  100. children: token.children,
  101. className: [...token.types].join(' '),
  102. }))
  103. );
  104. };
  105. /**
  106. * Returns a list of tokens broken up by line for syntax highlighting.
  107. *
  108. * Meant to be used for code blocks which require custom UI and cannot rely
  109. * on Prism.highlightElement().
  110. *
  111. * Each token contains a `className` and `children` which can be used for
  112. * rendering like so: <span className={token.className}>{token.children}</span>
  113. *
  114. * Automatically handles importing of the language grammar.
  115. */
  116. export const usePrismTokens = ({
  117. code,
  118. language,
  119. }: PrismHighlightParams): SyntaxHighlightLine[] => {
  120. const [grammar, setGrammar] = useState<Prism.Grammar | null>(() =>
  121. getPrismGrammar(language)
  122. );
  123. const onLoad = useCallback(() => {
  124. setGrammar(getPrismGrammar(language));
  125. }, [language]);
  126. useLoadPrismLanguage(language, {onLoad});
  127. const lines = useMemo(() => {
  128. try {
  129. if (!grammar) {
  130. return breakTokensByLine([code]);
  131. }
  132. const tokens = Prism.tokenize(code, grammar);
  133. return breakTokensByLine(tokens);
  134. } catch (e) {
  135. Sentry.captureException(e);
  136. return [];
  137. }
  138. }, [grammar, code]);
  139. return lines;
  140. };