1
0

test.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. import { test } from 'uvu'
  2. import * as assert from 'uvu/assert'
  3. import { visit } from 'unist-util-visit'
  4. import { rehype } from 'rehype'
  5. import { unified } from 'unified'
  6. import remarkParse from 'remark-parse'
  7. import remarkRehype from 'remark-rehype'
  8. import rehypeStringify from 'rehype-stringify'
  9. import dedent from 'dedent'
  10. import rehypePrism from './src/index.js'
  11. /**
  12. * Mock meta in code block
  13. */
  14. const addMeta = (metastring) => {
  15. if (!metastring) return
  16. return (tree) => {
  17. visit(tree, 'element', (node, index, parent) => {
  18. if (node.tagName === 'code') {
  19. node.data = { meta: metastring }
  20. }
  21. })
  22. }
  23. }
  24. const processHtml = (html, options, metastring) => {
  25. return rehype()
  26. .data('settings', { fragment: true })
  27. .use(addMeta, metastring)
  28. .use(rehypePrism, options)
  29. .processSync(html)
  30. .toString()
  31. }
  32. const processHtmlUnified = (html, options, metastring) => {
  33. return unified()
  34. .use(remarkParse)
  35. .use(remarkRehype, {})
  36. .use(addMeta, metastring)
  37. .use(rehypePrism, options)
  38. .use(rehypeStringify)
  39. .processSync(html)
  40. .toString()
  41. }
  42. test('adds a code-highlight class to the code and pre tag', () => {
  43. const result = processHtml(dedent`
  44. <pre><code class="language-py"></code></pre>
  45. `)
  46. const expected = dedent`<pre class="language-py"><code class="language-py code-highlight"></code></pre>`
  47. assert.is(result, expected)
  48. })
  49. test('add span with class code line for each line', () => {
  50. const result = processHtml(
  51. dedent`
  52. <pre><code>x = 6</code></pre>
  53. `
  54. )
  55. const expected = dedent`<pre><code class="code-highlight"><span class="code-line">x = 6</span></code></pre>`
  56. assert.is(result, expected)
  57. })
  58. test('finds code and highlights', () => {
  59. const result = processHtml(dedent`
  60. <div>
  61. <p>foo</p>
  62. <pre><code class="language-py">x = 6</code></pre>
  63. </div>
  64. `).trim()
  65. const expected = dedent`
  66. <div>
  67. <p>foo</p>
  68. <pre class="language-py"><code class="language-py code-highlight"><span class="code-line">x <span class="token operator">=</span> <span class="token number">6</span></span></code></pre>
  69. </div>
  70. `
  71. assert.is(result, expected)
  72. })
  73. test('respects line spacing', () => {
  74. const result = processHtml(dedent`
  75. <div>
  76. <pre><code class="language-py">x
  77. y
  78. </code></pre>
  79. </div>
  80. `).trim()
  81. const expected = dedent`
  82. <div>
  83. <pre class="language-py"><code class="language-py code-highlight"><span class="code-line">x
  84. </span><span class="code-line">
  85. </span><span class="code-line">y
  86. </span></code></pre>
  87. </div>
  88. `
  89. assert.is(result, expected)
  90. })
  91. test('handles uppercase correctly', () => {
  92. const result = processHtml(dedent`
  93. <div>
  94. <p>foo</p>
  95. <pre><code class="language-PY">x = 6</code></pre>
  96. </div>
  97. `).trim()
  98. const expected = dedent`
  99. <div>
  100. <p>foo</p>
  101. <pre class="language-py"><code class="language-PY code-highlight"><span class="code-line">x <span class="token operator">=</span> <span class="token number">6</span></span></code></pre>
  102. </div>
  103. `
  104. assert.is(result, expected)
  105. })
  106. test('each line of code should be a separate div', async () => {
  107. const result = processHtml(dedent`
  108. <div>
  109. <p>foo</p>
  110. <pre class="language-py">
  111. <code class="language-py code-highlight">x = 6
  112. y = 7
  113. </code>
  114. </pre>
  115. </div>
  116. `).trim()
  117. const codeLineCount = (result.match(/<span class="code-line">/g) || []).length
  118. assert.is(codeLineCount, 2)
  119. })
  120. test('should highlight line', async () => {
  121. const meta = '{1}'
  122. const result = processHtml(
  123. dedent`
  124. <div>
  125. <pre>
  126. <code class="language-py code-highlight">x = 6
  127. y = 7
  128. </code>
  129. </pre>
  130. </div>
  131. `,
  132. {},
  133. meta
  134. ).trim()
  135. const codeHighlightCount = (result.match(/<span class="code-line highlight-line">/g) || []).length
  136. assert.is(codeHighlightCount, 1)
  137. })
  138. test('should highlight comma separated lines', async () => {
  139. const meta = '{1,3}'
  140. const result = processHtml(
  141. dedent`
  142. <div>
  143. <pre>
  144. <code class="language-py code-highlight">x = 6
  145. y = 7
  146. z = 10
  147. </code>
  148. </pre>
  149. </div>
  150. `,
  151. {},
  152. meta
  153. ).trim()
  154. const codeHighlightCount = (result.match(/<span class="code-line highlight-line">/g) || []).length
  155. assert.is(codeHighlightCount, 2)
  156. })
  157. test('should should parse ranges with a space in between', async () => {
  158. const meta = '{1, 3}'
  159. const result = processHtml(
  160. dedent`
  161. <div>
  162. <pre>
  163. <code class="language-py code-highlight">x = 6
  164. y = 7
  165. z = 10
  166. </code>
  167. </pre>
  168. </div>
  169. `,
  170. {},
  171. meta
  172. ).trim()
  173. const codeHighlightCount = (result.match(/<span class="code-line highlight-line">/g) || []).length
  174. assert.is(codeHighlightCount, 2)
  175. })
  176. test('should highlight range separated lines', async () => {
  177. const meta = '{1-3}'
  178. const result = processHtml(
  179. dedent`
  180. <div>
  181. <pre>
  182. <code class="language-py code-highlight">x = 6
  183. y = 7
  184. z = 10
  185. </code>
  186. </pre>
  187. </div>
  188. `,
  189. {},
  190. meta
  191. ).trim()
  192. const codeHighlightCount = (result.match(/<span class="code-line highlight-line">/g) || []).length
  193. assert.is(codeHighlightCount, 3)
  194. })
  195. test('showLineNumbers option add line numbers', async () => {
  196. const result = processHtml(
  197. dedent`
  198. <div>
  199. <pre>
  200. <code class="language-py code-highlight">x = 6
  201. y = 7
  202. </code>
  203. </pre>
  204. </div>
  205. `,
  206. { showLineNumbers: true }
  207. ).trim()
  208. assert.ok(result.match(/line="1"/g))
  209. assert.ok(result.match(/line="2"/g))
  210. assert.not(result.match(/line="3"/g))
  211. })
  212. test('not show line number when showLineNumbers=false', async () => {
  213. const meta = 'showLineNumbers=false'
  214. const result = processHtml(
  215. dedent`
  216. <div>
  217. <pre>
  218. <code class="language-py code-highlight">x = 6
  219. y = 7
  220. </code>
  221. </pre>
  222. </div>
  223. `,
  224. { showLineNumbers: true },
  225. meta
  226. ).trim()
  227. assert.not(result.match(/line="1"/g))
  228. assert.not(result.match(/line="2"/g))
  229. })
  230. test('not show line number when showLineNumbers={false}', async () => {
  231. const meta = 'showLineNumbers={false}'
  232. const result = processHtml(
  233. dedent`
  234. <div>
  235. <pre>
  236. <code class="language-py code-highlight">x = 6
  237. y = 7
  238. </code>
  239. </pre>
  240. </div>
  241. `,
  242. { showLineNumbers: true },
  243. meta
  244. ).trim()
  245. assert.not(result.match(/line="1"/g))
  246. assert.not(result.match(/line="2"/g))
  247. })
  248. test('showLineNumbers property works in meta field', async () => {
  249. const meta = 'showLineNumbers'
  250. const result = processHtml(
  251. dedent`
  252. <div>
  253. <pre>
  254. <code class="language-py code-highlight">x = 6
  255. y = 7
  256. </code>
  257. </pre>
  258. </div>
  259. `,
  260. {},
  261. meta
  262. ).trim()
  263. assert.ok(result.match(/line="1"/g))
  264. assert.ok(result.match(/line="2"/g))
  265. assert.not(result.match(/line="3"/g))
  266. })
  267. test('showLineNumbers property with custom index works in meta field', async () => {
  268. const meta = 'showLineNumbers=5'
  269. const result = processHtml(
  270. dedent`
  271. <div>
  272. <pre>
  273. <code class="language-py code-highlight">x = 6
  274. y = 7
  275. </code>
  276. </pre>
  277. </div>
  278. `,
  279. {},
  280. meta
  281. ).trim()
  282. assert.ok(result.match(/line="5"/g))
  283. assert.ok(result.match(/line="6"/g))
  284. assert.not(result.match(/line="7"/g))
  285. })
  286. test('should support both highlighting and add line number', async () => {
  287. const meta = '{1} showLineNumbers'
  288. const result = processHtml(
  289. dedent`
  290. <div>
  291. <pre>
  292. <code class="language-py">x = 6
  293. y = 7
  294. z = 10
  295. </code>
  296. </pre>
  297. </div>
  298. `,
  299. {},
  300. meta
  301. ).trim()
  302. const codeHighlightCount = (result.match(/highlight-line/g) || []).length
  303. assert.is(codeHighlightCount, 1)
  304. assert.ok(result.match(/line="1"/g))
  305. assert.ok(result.match(/line="2"/g))
  306. })
  307. test('throw error with fake language- class', () => {
  308. assert.throws(
  309. () =>
  310. processHtml(dedent`
  311. <pre><code class="language-thisisnotalanguage">x = 6</code></pre>
  312. `),
  313. /Unknown language/
  314. )
  315. })
  316. test('with options.ignoreMissing, does nothing to code block with fake language- class', () => {
  317. const result = processHtml(
  318. dedent`
  319. <pre><code class="language-thisisnotalanguage">x = 6</code></pre>
  320. `,
  321. { ignoreMissing: true }
  322. )
  323. const expected = dedent`<pre><code class="language-thisisnotalanguage code-highlight"><span class="code-line">x = 6</span></code></pre>`
  324. assert.is(result, expected)
  325. })
  326. test('with options.defaultLanguage, it adds the correct language class tag', () => {
  327. const result = processHtml(
  328. dedent`
  329. <pre><code>x = 6</code></pre>
  330. `,
  331. { defaultLanguage: 'py' }
  332. )
  333. const expected = dedent`<pre class="language-py"><code class="language-py code-highlight"><span class="code-line">x <span class="token operator">=</span> <span class="token number">6</span></span></code></pre>`
  334. assert.is(result, expected)
  335. })
  336. test('defaultLanguage should produce the same syntax tree as if manually specified', () => {
  337. const resultDefaultLanguage = processHtml(
  338. dedent`
  339. <pre><code>x = 6</code></pre>
  340. `,
  341. { defaultLanguage: 'py' }
  342. )
  343. const resultManuallySpecified = processHtml(
  344. dedent`
  345. <pre><code class="language-py">x = 6</code></pre>
  346. `
  347. )
  348. assert.is(resultDefaultLanguage, resultManuallySpecified)
  349. })
  350. test('throws error if options.defaultLanguage is not registered with refractor', () => {
  351. assert.throws(
  352. () =>
  353. processHtml(
  354. dedent`
  355. <pre><code>x = 6</code></pre>
  356. `,
  357. { defaultLanguage: 'pyzqt' }
  358. ),
  359. /"pyzqt" is not registered with refractor/
  360. )
  361. })
  362. test('should work with multiline code / comments', () => {
  363. const result = processHtml(
  364. dedent`
  365. <pre><code class="language-js">
  366. /**
  367. * My comment
  368. */
  369. </code></pre>
  370. `,
  371. { ignoreMissing: true }
  372. )
  373. const expected = dedent`<pre class="language-js"><code class="language-js code-highlight"><span class="code-line">
  374. </span><span class="code-line"><span class="token doc-comment comment">/**
  375. </span></span><span class="code-line"><span class="token doc-comment comment"> * My comment
  376. </span></span><span class="code-line"><span class="token doc-comment comment"> */</span>
  377. </span></code></pre>`
  378. assert.is(result, expected)
  379. })
  380. test('adds inserted or deleted to code-line if lang=diff', async () => {
  381. const result = processHtml(
  382. dedent`
  383. <div>
  384. <pre>
  385. <code class="language-diff">+ x = 6
  386. - y = 7
  387. z = 10
  388. </code>
  389. </pre>
  390. </div>
  391. `
  392. ).trim()
  393. assert.ok(result.includes(`<span class="code-line inserted">`))
  394. assert.ok(result.includes(`<span class="code-line deleted">`))
  395. assert.ok(result.includes(`<span class="code-line">`))
  396. })
  397. test('works as a remarkjs / unifiedjs plugin', () => {
  398. const result = processHtmlUnified(
  399. dedent`
  400. ~~~jsx
  401. <Component/>
  402. ~~~
  403. `,
  404. { ignoreMissing: true }
  405. )
  406. const expected = dedent`<pre class="language-jsx"><code class="language-jsx code-highlight"><span class="code-line"><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span><span class="token class-name">Component</span></span><span class="token punctuation">/></span></span>
  407. </span></code></pre>`
  408. assert.is(result, expected)
  409. })
  410. test('diff and code highlighting should work together', () => {
  411. const result = processHtml(
  412. dedent`
  413. <pre><code class="language-diff-css">
  414. .hello{
  415. - background:url('./urel.png');
  416. + background-image:url('./urel.png');
  417. }
  418. </code></pre>
  419. `,
  420. { ignoreMissing: true }
  421. )
  422. assert.ok(result.includes(`<pre class="language-css">`))
  423. assert.ok(result.includes(`<span class="code-line inserted">`))
  424. assert.ok(result.includes(`<span class="code-line deleted">`))
  425. assert.ok(result.includes(`<span class="code-line">`))
  426. })
  427. test.run()