mdast-util-growi-directive.test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. import { fromMarkdown } from 'mdast-util-from-markdown';
  2. import { toMarkdown } from 'mdast-util-to-markdown';
  3. import { removePosition } from 'unist-util-remove-position';
  4. import { describe, expect, it } from 'vitest';
  5. import {
  6. directiveFromMarkdown,
  7. directiveToMarkdown,
  8. } from '../src/mdast-util-growi-directive/index.js';
  9. import { DirectiveType } from '../src/mdast-util-growi-directive/lib/index.js';
  10. import { directive } from '../src/micromark-extension-growi-directive/index.js';
  11. describe('markdown -> mdast', () => {
  12. it('should support directives (text)', () => {
  13. expect(
  14. fromMarkdown('a $b[c](d) e.', {
  15. extensions: [directive()],
  16. mdastExtensions: [directiveFromMarkdown()],
  17. }).children[0],
  18. ).toEqual({
  19. type: 'paragraph',
  20. children: [
  21. {
  22. type: 'text',
  23. value: 'a ',
  24. position: {
  25. start: { line: 1, column: 1, offset: 0 },
  26. end: { line: 1, column: 3, offset: 2 },
  27. },
  28. },
  29. {
  30. type: DirectiveType.Text,
  31. name: 'b',
  32. attributes: { d: '' },
  33. children: [
  34. {
  35. type: 'text',
  36. value: 'c',
  37. position: {
  38. start: { line: 1, column: 6, offset: 5 },
  39. end: { line: 1, column: 7, offset: 6 },
  40. },
  41. },
  42. ],
  43. position: {
  44. start: { line: 1, column: 3, offset: 2 },
  45. end: { line: 1, column: 11, offset: 10 },
  46. },
  47. },
  48. {
  49. type: 'text',
  50. value: ' e.',
  51. position: {
  52. start: { line: 1, column: 11, offset: 10 },
  53. end: { line: 1, column: 14, offset: 13 },
  54. },
  55. },
  56. ],
  57. position: {
  58. start: { line: 1, column: 1, offset: 0 },
  59. end: { line: 1, column: 14, offset: 13 },
  60. },
  61. });
  62. });
  63. it('should support directives (leaf)', () => {
  64. expect(
  65. fromMarkdown('$a[b](c)', {
  66. extensions: [directive()],
  67. mdastExtensions: [directiveFromMarkdown()],
  68. }).children[0],
  69. ).toEqual({
  70. type: DirectiveType.Leaf,
  71. name: 'a',
  72. attributes: { c: '' },
  73. children: [
  74. {
  75. type: 'text',
  76. value: 'b',
  77. position: {
  78. start: { line: 1, column: 4, offset: 3 },
  79. end: { line: 1, column: 5, offset: 4 },
  80. },
  81. },
  82. ],
  83. position: {
  84. start: { line: 1, column: 1, offset: 0 },
  85. end: { line: 1, column: 9, offset: 8 },
  86. },
  87. });
  88. });
  89. it('should support content in a label', () => {
  90. const tree = fromMarkdown('x $a[b *c*\nd]', {
  91. extensions: [directive()],
  92. mdastExtensions: [directiveFromMarkdown()],
  93. });
  94. removePosition(tree, { force: true });
  95. expect(tree).toEqual({
  96. type: 'root',
  97. children: [
  98. {
  99. type: 'paragraph',
  100. children: [
  101. { type: 'text', value: 'x ' },
  102. {
  103. type: DirectiveType.Text,
  104. name: 'a',
  105. attributes: {},
  106. children: [
  107. { type: 'text', value: 'b ' },
  108. { type: 'emphasis', children: [{ type: 'text', value: 'c' }] },
  109. { type: 'text', value: '\nd' },
  110. ],
  111. },
  112. ],
  113. },
  114. ],
  115. });
  116. });
  117. it('should support attributes', () => {
  118. const tree = fromMarkdown('x $a(#b.c.d e=f g="h&i&unknown;j")', {
  119. extensions: [directive()],
  120. mdastExtensions: [directiveFromMarkdown()],
  121. });
  122. removePosition(tree, { force: true });
  123. expect(tree).toEqual({
  124. type: 'root',
  125. children: [
  126. {
  127. type: 'paragraph',
  128. children: [
  129. { type: 'text', value: 'x ' },
  130. {
  131. type: DirectiveType.Text,
  132. name: 'a',
  133. attributes: {
  134. '#b.c.d': '',
  135. e: 'f',
  136. g: 'h&i&unknown;j',
  137. },
  138. children: [],
  139. },
  140. ],
  141. },
  142. ],
  143. });
  144. });
  145. it('should support EOLs in attributes', () => {
  146. const tree = fromMarkdown('$a(b\nc="d\ne")', {
  147. extensions: [directive()],
  148. mdastExtensions: [directiveFromMarkdown()],
  149. });
  150. removePosition(tree, { force: true });
  151. expect(tree).toEqual({
  152. type: 'root',
  153. children: [
  154. {
  155. type: 'paragraph',
  156. children: [
  157. {
  158. type: DirectiveType.Text,
  159. name: 'a',
  160. attributes: { b: '', c: 'd\ne' },
  161. children: [],
  162. },
  163. ],
  164. },
  165. ],
  166. });
  167. });
  168. });
  169. describe('mdast -> markdown', () => {
  170. it('should try to serialize a directive (text) w/o `name`', () => {
  171. expect(
  172. toMarkdown(
  173. {
  174. type: 'paragraph',
  175. children: [
  176. { type: 'text', value: 'a ' },
  177. // @ts-expect-error: `children`, `name` missing.
  178. { type: DirectiveType.Text },
  179. { type: 'text', value: ' b.' },
  180. ],
  181. },
  182. { extensions: [directiveToMarkdown()] },
  183. ),
  184. ).toBe('a $ b.\n');
  185. });
  186. it('should serialize a directive (text) w/ `name`', () => {
  187. expect(
  188. toMarkdown(
  189. {
  190. type: 'paragraph',
  191. children: [
  192. { type: 'text', value: 'a ' },
  193. // @ts-expect-error: `children` missing.
  194. { type: DirectiveType.Text, name: 'b' },
  195. { type: 'text', value: ' c.' },
  196. ],
  197. },
  198. { extensions: [directiveToMarkdown()] },
  199. ),
  200. ).toBe('a $b c.\n');
  201. });
  202. it('should serialize a directive (text) w/ `children`', () => {
  203. expect(
  204. toMarkdown(
  205. {
  206. type: 'paragraph',
  207. children: [
  208. { type: 'text', value: 'a ' },
  209. {
  210. type: DirectiveType.Text,
  211. name: 'b',
  212. children: [{ type: 'text', value: 'c' }],
  213. },
  214. { type: 'text', value: ' d.' },
  215. ],
  216. },
  217. { extensions: [directiveToMarkdown()] },
  218. ),
  219. ).toBe('a $b[c] d.\n');
  220. });
  221. it('should escape brackets in a directive (text) label', () => {
  222. expect(
  223. toMarkdown(
  224. {
  225. type: 'paragraph',
  226. children: [
  227. { type: 'text', value: 'a ' },
  228. {
  229. type: DirectiveType.Text,
  230. name: 'b',
  231. children: [{ type: 'text', value: 'c[d]e' }],
  232. },
  233. { type: 'text', value: ' f.' },
  234. ],
  235. },
  236. { extensions: [directiveToMarkdown()] },
  237. ),
  238. ).toBe('a $b[c\\[d\\]e] f.\n');
  239. });
  240. it('should support EOLs in a directive (text) label', () => {
  241. expect(
  242. toMarkdown(
  243. {
  244. type: 'paragraph',
  245. children: [
  246. { type: 'text', value: 'a ' },
  247. {
  248. type: DirectiveType.Text,
  249. name: 'b',
  250. children: [{ type: 'text', value: 'c\nd' }],
  251. },
  252. { type: 'text', value: ' e.' },
  253. ],
  254. },
  255. { extensions: [directiveToMarkdown()] },
  256. ),
  257. ).toBe('a $b[c\nd] e.\n');
  258. });
  259. it('should serialize a directive (text) w/ `attributes`', () => {
  260. expect(
  261. toMarkdown(
  262. {
  263. type: 'paragraph',
  264. children: [
  265. { type: 'text', value: 'a ' },
  266. {
  267. type: DirectiveType.Text,
  268. name: 'b',
  269. // @ts-expect-error: should contain only `string`s
  270. attributes: {
  271. c: 'd',
  272. e: 'f',
  273. g: '',
  274. h: null,
  275. i: undefined,
  276. j: 2,
  277. },
  278. children: [],
  279. },
  280. { type: 'text', value: ' k.' },
  281. ],
  282. },
  283. { extensions: [directiveToMarkdown()] },
  284. ),
  285. ).toBe('a $b(c="d" e="f" g j="2") k.\n');
  286. });
  287. it('should serialize a directive (text) w/ hash, dot notation attributes', () => {
  288. expect(
  289. toMarkdown(
  290. {
  291. type: 'paragraph',
  292. children: [
  293. { type: 'text', value: 'a ' },
  294. {
  295. type: DirectiveType.Text,
  296. name: 'b',
  297. attributes: { '#d': '', '.a.b.c': '', key: 'value' },
  298. children: [],
  299. },
  300. { type: 'text', value: ' k.' },
  301. ],
  302. },
  303. { extensions: [directiveToMarkdown()] },
  304. ),
  305. ).toBe('a $b(#d .a.b.c key="value") k.\n');
  306. });
  307. it('should encode the quote in an attribute value (text)', () => {
  308. expect(
  309. toMarkdown(
  310. {
  311. type: 'paragraph',
  312. children: [
  313. { type: 'text', value: 'a ' },
  314. {
  315. type: DirectiveType.Text,
  316. name: 'b',
  317. attributes: { x: 'y"\'\r\nz' },
  318. children: [],
  319. },
  320. { type: 'text', value: ' k.' },
  321. ],
  322. },
  323. { extensions: [directiveToMarkdown()] },
  324. ),
  325. ).toBe('a $b(x="y"\'\r\nz") k.\n');
  326. });
  327. it('should not use the `id` shortcut if impossible characters exist', () => {
  328. expect(
  329. toMarkdown(
  330. {
  331. type: 'paragraph',
  332. children: [
  333. { type: 'text', value: 'a ' },
  334. {
  335. type: DirectiveType.Text,
  336. name: 'b',
  337. attributes: { id: 'c#d' },
  338. children: [],
  339. },
  340. { type: 'text', value: ' e.' },
  341. ],
  342. },
  343. { extensions: [directiveToMarkdown()] },
  344. ),
  345. ).toBe('a $b(id="c#d") e.\n');
  346. });
  347. it('should not use the `class` shortcut if impossible characters exist', () => {
  348. expect(
  349. toMarkdown(
  350. {
  351. type: 'paragraph',
  352. children: [
  353. { type: 'text', value: 'a ' },
  354. {
  355. type: DirectiveType.Text,
  356. name: 'b',
  357. attributes: { 'c.d': '', 'e<f': '' },
  358. children: [],
  359. },
  360. { type: 'text', value: ' g.' },
  361. ],
  362. },
  363. { extensions: [directiveToMarkdown()] },
  364. ),
  365. ).toBe('a $b(c.d e<f) g.\n');
  366. });
  367. it("should not use the `class` shortcut if impossible characters exist (but should use it for classes that don't)", () => {
  368. expect(
  369. toMarkdown(
  370. {
  371. type: 'paragraph',
  372. children: [
  373. { type: 'text', value: 'a ' },
  374. {
  375. type: DirectiveType.Text,
  376. name: 'b',
  377. attributes: {
  378. 'c.d': '',
  379. e: '',
  380. 'f<g': '',
  381. hij: '',
  382. },
  383. children: [],
  384. },
  385. { type: 'text', value: ' k.' },
  386. ],
  387. },
  388. { extensions: [directiveToMarkdown()] },
  389. ),
  390. ).toBe('a $b(c.d e f<g hij) k.\n');
  391. });
  392. it('should try to serialize a directive (leaf) w/o `name`', () => {
  393. // @ts-expect-error: `children`, `name` missing.
  394. expect(
  395. toMarkdown(
  396. { type: DirectiveType.Leaf },
  397. { extensions: [directiveToMarkdown()] },
  398. ),
  399. ).toBe('$\n');
  400. });
  401. it('should serialize a directive (leaf) w/ `name`', () => {
  402. // @ts-expect-error: `children` missing.
  403. expect(
  404. toMarkdown(
  405. { type: DirectiveType.Leaf, name: 'a' },
  406. { extensions: [directiveToMarkdown()] },
  407. ),
  408. ).toBe('$a\n');
  409. });
  410. it('should serialize a directive (leaf) w/ `children`', () => {
  411. expect(
  412. toMarkdown(
  413. {
  414. type: DirectiveType.Leaf,
  415. name: 'a',
  416. children: [{ type: 'text', value: 'b' }],
  417. },
  418. { extensions: [directiveToMarkdown()] },
  419. ),
  420. ).toBe('$a[b]\n');
  421. });
  422. it('should serialize a directive (leaf) w/ EOLs in `children`', () => {
  423. expect(
  424. toMarkdown(
  425. {
  426. type: DirectiveType.Leaf,
  427. name: 'a',
  428. children: [{ type: 'text', value: 'b\nc' }],
  429. },
  430. { extensions: [directiveToMarkdown()] },
  431. ),
  432. ).toBe('$a[b&#xA;c]\n');
  433. });
  434. it('should serialize a directive (leaf) w/ EOLs in `attributes`', () => {
  435. expect(
  436. toMarkdown(
  437. {
  438. type: DirectiveType.Leaf,
  439. name: 'a',
  440. attributes: { '#b': '', '.c.d': '', key: 'e\nf' },
  441. children: [],
  442. },
  443. { extensions: [directiveToMarkdown()] },
  444. ),
  445. ).toBe('$a(#b .c.d key="e&#xA;f")\n');
  446. });
  447. it('should escape a `:` in phrasing when followed by an alpha', () => {
  448. expect(
  449. toMarkdown(
  450. {
  451. type: 'paragraph',
  452. children: [{ type: 'text', value: 'a$b' }],
  453. },
  454. { extensions: [directiveToMarkdown()] },
  455. ),
  456. ).toBe('a\\$b\n');
  457. });
  458. it('should not escape a `:` in phrasing when followed by a non-alpha', () => {
  459. expect(
  460. toMarkdown(
  461. {
  462. type: 'paragraph',
  463. children: [{ type: 'text', value: 'a$9' }],
  464. },
  465. { extensions: [directiveToMarkdown()] },
  466. ),
  467. ).toBe('a$9\n');
  468. });
  469. it('should not escape a `:` in phrasing when preceded by a colon', () => {
  470. expect(
  471. toMarkdown(
  472. {
  473. type: 'paragraph',
  474. children: [{ type: 'text', value: 'a$c' }],
  475. },
  476. { extensions: [directiveToMarkdown()] },
  477. ),
  478. ).toBe('a\\$c\n');
  479. });
  480. it('should not escape a `:` at a break', () => {
  481. expect(
  482. toMarkdown(
  483. {
  484. type: 'paragraph',
  485. children: [{ type: 'text', value: '$\na' }],
  486. },
  487. { extensions: [directiveToMarkdown()] },
  488. ),
  489. ).toBe('$\na\n');
  490. });
  491. it('should not escape a `:` at a break when followed by an alpha', () => {
  492. expect(
  493. toMarkdown(
  494. {
  495. type: 'paragraph',
  496. children: [{ type: 'text', value: '$a' }],
  497. },
  498. { extensions: [directiveToMarkdown()] },
  499. ),
  500. ).toBe('\\$a\n');
  501. });
  502. it('should escape a `:` at a break when followed by a colon', () => {
  503. expect(
  504. toMarkdown(
  505. {
  506. type: 'paragraph',
  507. children: [{ type: 'text', value: '$\na' }],
  508. },
  509. { extensions: [directiveToMarkdown()] },
  510. ),
  511. ).toBe('$\na\n');
  512. });
  513. it('should escape a `:` after a text directive', () => {
  514. expect(
  515. toMarkdown(
  516. {
  517. type: 'paragraph',
  518. children: [
  519. { type: DirectiveType.Text, name: 'red', children: [] },
  520. { type: 'text', value: '$' },
  521. ],
  522. },
  523. { extensions: [directiveToMarkdown()] },
  524. ),
  525. ).toBe('$red$\n');
  526. });
  527. });