snippet.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import {
  2. dedent,
  3. findRegion,
  4. rawPathToToken
  5. } from 'node/markdown/plugins/snippet'
  6. import { expect } from 'vitest'
  7. const removeEmptyKeys = <T extends Record<string, unknown>>(obj: T) => {
  8. return Object.fromEntries(
  9. Object.entries(obj).filter(([, value]) => value !== '')
  10. ) as T
  11. }
  12. /* prettier-ignore */
  13. const rawPathTokenMap: [string, Partial<{ filepath: string, extension: string, title: string, region: string, lines: string, lang: string }>][] = [
  14. ['/path/to/file.extension', { filepath: '/path/to/file.extension', extension: 'extension', title: 'file.extension' }],
  15. ['./path/to/file.extension', { filepath: './path/to/file.extension', extension: 'extension', title: 'file.extension' }],
  16. ['/path to/file.extension', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension' }],
  17. ['./path to/file.extension', { filepath: './path to/file.extension', extension: 'extension', title: 'file.extension' }],
  18. ['/path.to/file.extension', { filepath: '/path.to/file.extension', extension: 'extension', title: 'file.extension' }],
  19. ['./path.to/file.extension', { filepath: './path.to/file.extension', extension: 'extension', title: 'file.extension' }],
  20. ['/path .to/file.extension', { filepath: '/path .to/file.extension', extension: 'extension', title: 'file.extension' }],
  21. ['./path .to/file.extension', { filepath: './path .to/file.extension', extension: 'extension', title: 'file.extension' }],
  22. ['/path/to/file', { filepath: '/path/to/file', title: 'file' }],
  23. ['./path/to/file', { filepath: './path/to/file', title: 'file' }],
  24. ['/path to/file', { filepath: '/path to/file', title: 'file' }],
  25. ['./path to/file', { filepath: './path to/file', title: 'file' }],
  26. ['/path.to/file', { filepath: '/path.to/file', title: 'file' }],
  27. ['./path.to/file', { filepath: './path.to/file', title: 'file' }],
  28. ['/path .to/file', { filepath: '/path .to/file', title: 'file' }],
  29. ['./path .to/file', { filepath: './path .to/file', title: 'file' }],
  30. ['/path/to/file.extension#region', { filepath: '/path/to/file.extension', extension: 'extension', title: 'file.extension', region: '#region' }],
  31. ['./path/to/file.extension {c#}', { filepath: './path/to/file.extension', extension: 'extension', title: 'file.extension', lang: 'c#' }],
  32. ['/path to/file.extension {1,2,4-6}', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension', lines: '1,2,4-6' }],
  33. ['/path to/file.extension {1,2,4-6 c#}', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension', lines: '1,2,4-6', lang: 'c#' }],
  34. ['/path.to/file.extension [title]', { filepath: '/path.to/file.extension', extension: 'extension', title: 'title' }],
  35. ['./path.to/file.extension#region {c#}', { filepath: './path.to/file.extension', extension: 'extension', title: 'file.extension', region: '#region', lang: 'c#' }],
  36. ['/path/to/file#region {1,2,4-6}', { filepath: '/path/to/file', title: 'file', region: '#region', lines: '1,2,4-6' }],
  37. ['./path/to/file#region {1,2,4-6 c#}', { filepath: './path/to/file', title: 'file', region: '#region', lines: '1,2,4-6', lang: 'c#' }],
  38. ['/path to/file {1,2,4-6 c#} [title]', { filepath: '/path to/file', title: 'title', lines: '1,2,4-6', lang: 'c#' }],
  39. ['./path to/file#region {1,2,4-6 c#} [title]', { filepath: './path to/file', title: 'title', region: '#region', lines: '1,2,4-6', lang: 'c#' }],
  40. ]
  41. describe('node/markdown/plugins/snippet', () => {
  42. describe('dedent', () => {
  43. test('when 0-level is minimal, do not remove spaces', () => {
  44. expect(
  45. dedent(
  46. [
  47. //
  48. 'fn main() {',
  49. ' println!("Hello");',
  50. '}'
  51. ].join('\n')
  52. )
  53. ).toMatchInlineSnapshot(`
  54. "fn main() {
  55. println!("Hello");
  56. }"
  57. `)
  58. })
  59. test('when 4-level is minimal, remove 4 spaces', () => {
  60. expect(
  61. dedent(
  62. [
  63. //
  64. ' let a = {',
  65. ' value: 42',
  66. ' };'
  67. ].join('\n')
  68. )
  69. ).toMatchInlineSnapshot(`
  70. "let a = {
  71. value: 42
  72. };"
  73. `)
  74. })
  75. test('when only 1 line is passed, dedent it', () => {
  76. expect(dedent(' let a = 42;')).toEqual('let a = 42;')
  77. })
  78. test('handle tabs as well', () => {
  79. expect(
  80. dedent(
  81. [
  82. //
  83. ' let a = {',
  84. ' value: 42',
  85. ' };'
  86. ].join('\n')
  87. )
  88. ).toMatchInlineSnapshot(`
  89. "let a = {
  90. value: 42
  91. };"
  92. `)
  93. })
  94. })
  95. describe('rawPathToToken', () => {
  96. test.each(rawPathTokenMap)('%s', (rawPath, token) => {
  97. expect(removeEmptyKeys(rawPathToToken(rawPath))).toEqual(token)
  98. })
  99. })
  100. describe('findRegion', () => {
  101. it('returns null when no region markers are present', () => {
  102. const lines = ['function foo() {', ' console.log("hello");', '}']
  103. expect(findRegion(lines, 'foo')).toBeNull()
  104. })
  105. it('ignores non-matching region names', () => {
  106. const lines = [
  107. '// #region regionA',
  108. 'some code here',
  109. '// #endregion regionA'
  110. ]
  111. expect(findRegion(lines, 'regionC')).toBeNull()
  112. })
  113. it('returns null if a region start marker exists without a matching end marker', () => {
  114. const lines = [
  115. '// #region missingEnd',
  116. 'console.log("inside region");',
  117. 'console.log("still inside");'
  118. ]
  119. expect(findRegion(lines, 'missingEnd')).toBeNull()
  120. })
  121. it('returns null if an end marker exists without a preceding start marker', () => {
  122. const lines = [
  123. '// #endregion ghostRegion',
  124. 'console.log("stray end marker");'
  125. ]
  126. expect(findRegion(lines, 'ghostRegion')).toBeNull()
  127. })
  128. it('detects C#/JavaScript style region markers with matching tags', () => {
  129. const lines = [
  130. 'Console.WriteLine("Before region");',
  131. '#region hello',
  132. 'Console.WriteLine("Hello, World!");',
  133. '#endregion hello',
  134. 'Console.WriteLine("After region");'
  135. ]
  136. const result = findRegion(lines, 'hello')
  137. expect(result).not.toBeNull()
  138. if (result) {
  139. expect(lines.slice(result.start, result.end).join('\n')).toBe(
  140. 'Console.WriteLine("Hello, World!");'
  141. )
  142. }
  143. })
  144. it('detects region markers even when the end marker omits the region name', () => {
  145. const lines = [
  146. 'Console.WriteLine("Before region");',
  147. '#region hello',
  148. 'Console.WriteLine("Hello, World!");',
  149. '#endregion',
  150. 'Console.WriteLine("After region");'
  151. ]
  152. const result = findRegion(lines, 'hello')
  153. expect(result).not.toBeNull()
  154. if (result) {
  155. expect(lines.slice(result.start, result.end).join('\n')).toBe(
  156. 'Console.WriteLine("Hello, World!");'
  157. )
  158. }
  159. })
  160. it('handles indented region markers correctly', () => {
  161. const lines = [
  162. ' Console.WriteLine("Before region");',
  163. ' #region hello',
  164. ' Console.WriteLine("Hello, World!");',
  165. ' #endregion hello',
  166. ' Console.WriteLine("After region");'
  167. ]
  168. const result = findRegion(lines, 'hello')
  169. expect(result).not.toBeNull()
  170. if (result) {
  171. expect(lines.slice(result.start, result.end).join('\n')).toBe(
  172. ' Console.WriteLine("Hello, World!");'
  173. )
  174. }
  175. })
  176. it('detects TypeScript style region markers', () => {
  177. const lines = [
  178. 'let regexp: RegExp[] = [];',
  179. '// #region foo',
  180. 'let start = -1;',
  181. '// #endregion foo'
  182. ]
  183. const result = findRegion(lines, 'foo')
  184. expect(result).not.toBeNull()
  185. if (result) {
  186. expect(lines.slice(result.start, result.end).join('\n')).toBe(
  187. 'let start = -1;'
  188. )
  189. }
  190. })
  191. it('detects CSS style region markers', () => {
  192. const lines = [
  193. '.body-content {',
  194. '/* #region foo */',
  195. ' padding-left: 15px;',
  196. '/* #endregion foo */',
  197. ' padding-right: 15px;',
  198. '}'
  199. ]
  200. const result = findRegion(lines, 'foo')
  201. expect(result).not.toBeNull()
  202. if (result) {
  203. expect(lines.slice(result.start, result.end).join('\n')).toBe(
  204. ' padding-left: 15px;'
  205. )
  206. }
  207. })
  208. it('detects HTML style region markers', () => {
  209. const lines = [
  210. '<div>Some content</div>',
  211. '<!-- #region foo -->',
  212. ' <h1>Hello world</h1>',
  213. '<!-- #endregion foo -->',
  214. '<div>Other content</div>'
  215. ]
  216. const result = findRegion(lines, 'foo')
  217. expect(result).not.toBeNull()
  218. if (result) {
  219. expect(lines.slice(result.start, result.end).join('\n')).toBe(
  220. ' <h1>Hello world</h1>'
  221. )
  222. }
  223. })
  224. it('detects Visual Basic style region markers (with case-insensitive "End")', () => {
  225. const lines = [
  226. 'Console.WriteLine("VB")',
  227. '#Region VBRegion',
  228. ' Console.WriteLine("Inside region")',
  229. '#End Region VBRegion',
  230. 'Console.WriteLine("Done")'
  231. ]
  232. const result = findRegion(lines, 'VBRegion')
  233. expect(result).not.toBeNull()
  234. if (result) {
  235. expect(lines.slice(result.start, result.end).join('\n')).toBe(
  236. ' Console.WriteLine("Inside region")'
  237. )
  238. }
  239. })
  240. it('detects Bat style region markers', () => {
  241. const lines = ['::#region foo', 'echo off', '::#endregion foo']
  242. const result = findRegion(lines, 'foo')
  243. expect(result).not.toBeNull()
  244. if (result) {
  245. expect(lines.slice(result.start, result.end).join('\n')).toBe(
  246. 'echo off'
  247. )
  248. }
  249. })
  250. it('detects C/C++ style region markers using #pragma', () => {
  251. const lines = [
  252. '#pragma region foo',
  253. 'int a = 1;',
  254. '#pragma endregion foo'
  255. ]
  256. const result = findRegion(lines, 'foo')
  257. expect(result).not.toBeNull()
  258. if (result) {
  259. expect(lines.slice(result.start, result.end).join('\n')).toBe(
  260. 'int a = 1;'
  261. )
  262. }
  263. })
  264. it('returns the first complete region when multiple regions exist', () => {
  265. const lines = [
  266. '// #region foo',
  267. 'first region content',
  268. '// #endregion foo',
  269. '// #region foo',
  270. 'second region content',
  271. '// #endregion foo'
  272. ]
  273. const result = findRegion(lines, 'foo')
  274. expect(result).not.toBeNull()
  275. if (result) {
  276. expect(lines.slice(result.start, result.end).join('\n')).toBe(
  277. 'first region content'
  278. )
  279. }
  280. })
  281. it('handles nested regions with different names properly', () => {
  282. const lines = [
  283. '// #region foo',
  284. "console.log('line before nested');",
  285. '// #region bar',
  286. "console.log('nested content');",
  287. '// #endregion bar',
  288. '// #endregion foo'
  289. ]
  290. const result = findRegion(lines, 'foo')
  291. expect(result).not.toBeNull()
  292. if (result) {
  293. const extracted = lines.slice(result.start, result.end).join('\n')
  294. const expected = [
  295. "console.log('line before nested');",
  296. '// #region bar',
  297. "console.log('nested content');",
  298. '// #endregion bar'
  299. ].join('\n')
  300. expect(extracted).toBe(expected)
  301. }
  302. })
  303. })
  304. })