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

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