Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

markdown-utils.ts 3.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. /**
  2. * @fileoverview Utility functions for preprocessing Markdown content.
  3. * These functions were extracted from the main markdown renderer for better separation of concerns.
  4. * Includes preprocessing for LaTeX and custom "think" tags.
  5. */
  6. import { flow } from 'lodash-es'
  7. export const preprocessLaTeX = (content: string) => {
  8. if (typeof content !== 'string')
  9. return content
  10. const codeBlockRegex = /```[\s\S]*?```/g
  11. const codeBlocks = content.match(codeBlockRegex) || []
  12. const escapeReplacement = (str: string) => str.replace(/\$/g, '_TMP_REPLACE_DOLLAR_')
  13. let processedContent = content.replace(codeBlockRegex, 'CODE_BLOCK_PLACEHOLDER')
  14. processedContent = flow([
  15. (str: string) => str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`),
  16. (str: string) => str.replace(/\\\[([\s\S]*?)\\\]/g, (_, equation) => `$$${equation}$$`),
  17. (str: string) => str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`),
  18. (str: string) => str.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`),
  19. ])(processedContent)
  20. codeBlocks.forEach((block) => {
  21. processedContent = processedContent.replace('CODE_BLOCK_PLACEHOLDER', escapeReplacement(block))
  22. })
  23. processedContent = processedContent.replace(/_TMP_REPLACE_DOLLAR_/g, '$')
  24. return processedContent
  25. }
  26. export const preprocessThinkTag = (content: string) => {
  27. const thinkOpenTagRegex = /(<think>\n)+/g
  28. const thinkCloseTagRegex = /\n<\/think>/g
  29. return flow([
  30. (str: string) => str.replace(thinkOpenTagRegex, '<details data-think=true>\n'),
  31. (str: string) => str.replace(thinkCloseTagRegex, '\n[ENDTHINKFLAG]</details>'),
  32. (str: string) => str.replace(/(<\/details>)(?![^\S\r\n]*[\r\n])(?![^\S\r\n]*$)/g, '$1\n'),
  33. ])(content)
  34. }
  35. /**
  36. * Transforms a URI for use in react-markdown, ensuring security and compatibility.
  37. * This function is designed to work with react-markdown v9+ which has stricter
  38. * default URL handling.
  39. *
  40. * Behavior:
  41. * 1. Always allows the custom 'abbr:' protocol.
  42. * 2. Always allows page-local fragments (e.g., "#some-id").
  43. * 3. Always allows protocol-relative URLs (e.g., "//example.com/path").
  44. * 4. Always allows purely relative paths (e.g., "path/to/file", "/abs/path").
  45. * 5. Allows absolute URLs if their scheme is in a permitted list (case-insensitive):
  46. * 'http:', 'https:', 'mailto:', 'xmpp:', 'irc:', 'ircs:'.
  47. * 6. Intelligently distinguishes colons used for schemes from colons within
  48. * paths, query parameters, or fragments of relative-like URLs.
  49. * 7. Returns the original URI if allowed, otherwise returns `undefined` to
  50. * signal that the URI should be removed/disallowed by react-markdown.
  51. */
  52. export const customUrlTransform = (uri: string): string | undefined => {
  53. const PERMITTED_SCHEME_REGEX = /^(https?|ircs?|mailto|xmpp|abbr):$/i
  54. if (uri.startsWith('#'))
  55. return uri
  56. if (uri.startsWith('//'))
  57. return uri
  58. const colonIndex = uri.indexOf(':')
  59. if (colonIndex === -1)
  60. return uri
  61. const slashIndex = uri.indexOf('/')
  62. const questionMarkIndex = uri.indexOf('?')
  63. const hashIndex = uri.indexOf('#')
  64. if (
  65. (slashIndex !== -1 && colonIndex > slashIndex)
  66. || (questionMarkIndex !== -1 && colonIndex > questionMarkIndex)
  67. || (hashIndex !== -1 && colonIndex > hashIndex)
  68. )
  69. return uri
  70. const scheme = uri.substring(0, colonIndex + 1).toLowerCase()
  71. if (PERMITTED_SCHEME_REGEX.test(scheme))
  72. return uri
  73. return undefined
  74. }