utest.markdown.parser.pas 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. {
  2. This file is part of the Free Component Library (FCL)
  3. Copyright (c) 2025 by Michael Van Canneyt
  4. Markdown block parser tests
  5. See the file COPYING.FPC, included in this distribution,
  6. for details about the copyright.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  10. **********************************************************************}
  11. unit UTest.Markdown.Parser;
  12. {$mode objfpc}{$H+}
  13. interface
  14. uses
  15. Classes, SysUtils, fpcunit, testregistry, Contnrs,
  16. Markdown.Elements, Markdown.Parser;
  17. type
  18. { TBlockTestCase }
  19. // Helper base class to avoid boilerplate code
  20. TBlockTestCase = class(TTestCase)
  21. private
  22. FDoc: TMarkDownDocument;
  23. FParser: TMarkDownParser;
  24. FStrings: TStringList;
  25. procedure CheckTextnodeText(const aMsg: string; aBlock: TMarkDownBlock; const aText: string);
  26. protected
  27. procedure SetupParser(const AText: String);
  28. procedure CheckBlockText(const aMsg: string; aBlock: TMarkDownBlock; const aText : string; aInParagraph: Boolean);
  29. function GetBlock(AIndex: Integer): TMarkDownBlock;
  30. property Doc: TMarkDownDocument read FDoc;
  31. public
  32. procedure SetUp; override;
  33. procedure TearDown; override;
  34. end;
  35. { TTestParagraphs }
  36. TTestParagraphs = class(TBlockTestCase)
  37. published
  38. procedure TestSimpleParagraph;
  39. procedure TestMultipleParagraphs;
  40. end;
  41. { TTestHeadings }
  42. TTestHeadings = class(TBlockTestCase)
  43. published
  44. procedure TestATXHeading;
  45. procedure TestSetextHeadings;
  46. end;
  47. { TTestCodeBlocks }
  48. TTestCodeBlocks = class(TBlockTestCase)
  49. published
  50. procedure TestIndentedCodeBlock;
  51. procedure TestFencedCodeBlock;
  52. procedure TestFencedCodeBlockWithInfoString;
  53. end;
  54. { TTestBlockQuotes }
  55. TTestBlockQuotes = class(TBlockTestCase)
  56. published
  57. procedure TestSimpleQuote;
  58. procedure TestNestedQuote;
  59. procedure TestLazy;
  60. end;
  61. { TTestLists }
  62. TTestLists = class(TBlockTestCase)
  63. published
  64. procedure TestUnorderedList;
  65. procedure TestOrderedList;
  66. procedure TestNestedList;
  67. end;
  68. { TTestThematicBreaks }
  69. TTestThematicBreaks = class(TBlockTestCase)
  70. published
  71. procedure TestAsteriskBreak;
  72. procedure TestUnderscoreBreak;
  73. end;
  74. { TTestTables }
  75. TTestTables = class(TBlockTestCase)
  76. published
  77. procedure TestSimpleTable;
  78. end;
  79. implementation
  80. { TBlockTestCase }
  81. procedure TBlockTestCase.SetUp;
  82. begin
  83. inherited SetUp;
  84. FStrings := TStringList.Create;
  85. FParser := TMarkDownParser.Create(nil);
  86. end;
  87. procedure TBlockTestCase.TearDown;
  88. begin
  89. FDoc.Free;
  90. FParser.Free;
  91. FStrings.Free;
  92. inherited TearDown;
  93. end;
  94. procedure TBlockTestCase.SetupParser(const AText: String);
  95. begin
  96. FStrings.Text := AText;
  97. FDoc := FParser.Parse(FStrings);
  98. // FDoc.Dump('');
  99. AssertNotNull('Document should be parsed', FDoc);
  100. end;
  101. procedure TBlockTestCase.CheckBlockText(Const aMsg : string; aBlock: TMarkDownBlock; const aText : String; aInParagraph: Boolean);
  102. var
  103. lBlock : TMarkDownBlock;
  104. begin
  105. lBlock:=aBlock;
  106. AssertTrue(aMsg+': Have child',lBlock.ChildCount>0);
  107. if aInParagraph then
  108. begin
  109. lBlock:=lBlock[0];
  110. AssertEquals(aMsg+': child is para',TMarkDownParagraphBlock,lBlock.ClassType);
  111. AssertTrue(aMsg+': Paragrapg Has child',lBlock.ChildCount>0);
  112. end;
  113. lBlock:=lBlock[0];
  114. CheckTextnodeText(aMsg,lBlock,aText);
  115. end;
  116. procedure TBlockTestCase.CheckTextnodeText(const aMsg : string; aBlock : TMarkDownBlock; const aText : string);
  117. var
  118. lText : TMarkDownTextBlock absolute aBlock;
  119. lTextNode : TMarkDownTextNode;
  120. lCount : Integer;
  121. begin
  122. AssertEquals(aMsg+': block is text',TMarkDownTextBlock,aBlock.ClassType);
  123. lCount:=lText.Nodes.Count;
  124. AssertTrue(aMsg+' text nodes',lCount>0);
  125. lTextNode:=lText.Nodes[0];
  126. AssertEquals(aMsg+' text node text',aText,lTextNode.NodeText);
  127. end;
  128. function TBlockTestCase.GetBlock(AIndex: Integer): TMarkDownBlock;
  129. begin
  130. AssertTrue('Block index out of bounds', AIndex < FDoc.Blocks.Count);
  131. Result := FDoc.Blocks[AIndex];
  132. end;
  133. { TTestParagraphs }
  134. procedure TTestParagraphs.TestSimpleParagraph;
  135. var
  136. Block: TMarkDownParagraphBlock;
  137. begin
  138. SetupParser('This is a simple paragraph.');
  139. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  140. Block := GetBlock(0) as TMarkDownParagraphBlock;
  141. AssertNotNull('Block should be a paragraph', Block);
  142. AssertTrue('Should be a plain paragraph', Block.isPlainPara);
  143. end;
  144. procedure TTestParagraphs.TestMultipleParagraphs;
  145. begin
  146. SetupParser('First paragraph.'#10#10'Second paragraph.');
  147. AssertEquals('Document should have 2 blocks', 2, Doc.Blocks.Count);
  148. AssertTrue('First block should be a paragraph', GetBlock(0) is TMarkDownParagraphBlock);
  149. AssertTrue('Second block should be a paragraph', GetBlock(1) is TMarkDownParagraphBlock);
  150. end;
  151. { TTestHeadings }
  152. procedure TTestHeadings.TestATXHeading;
  153. var
  154. Block: TMarkDownHeadingBlock;
  155. begin
  156. SetupParser('# A Level 1 Heading');
  157. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  158. Block := GetBlock(0) as TMarkDownHeadingBlock;
  159. AssertNotNull('Block should be a heading', Block);
  160. AssertEquals('Heading level should be 1', 1, Block.Level);
  161. end;
  162. procedure TTestHeadings.TestSetextHeadings;
  163. var
  164. Block: TMarkDownParagraphBlock;
  165. begin
  166. SetupParser('A Level 2 Heading'#10'-----------------');
  167. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  168. Block := GetBlock(0) as TMarkDownParagraphBlock;
  169. AssertNotNull('Block should be a paragraph (used for setext)', Block);
  170. AssertEquals('Header property should be 2 for setext', 2, Block.Header);
  171. end;
  172. { TTestCodeBlocks }
  173. procedure TTestCodeBlocks.TestIndentedCodeBlock;
  174. var
  175. Block: TMarkDownCodeBlock;
  176. begin
  177. SetupParser(' a = 1;'#10' b = 2;');
  178. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  179. Block := GetBlock(0) as TMarkDownCodeBlock;
  180. AssertNotNull('Block should be a code block', Block);
  181. AssertFalse('Should not be a fenced code block', Block.Fenced);
  182. end;
  183. procedure TTestCodeBlocks.TestFencedCodeBlock;
  184. var
  185. Block: TMarkDownCodeBlock;
  186. begin
  187. SetupParser('```'#10'code here'#10'```');
  188. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  189. Block := GetBlock(0) as TMarkDownCodeBlock;
  190. AssertNotNull('Block should be a code block', Block);
  191. AssertTrue('Should be a fenced code block', Block.Fenced);
  192. end;
  193. procedure TTestCodeBlocks.TestFencedCodeBlockWithInfoString;
  194. var
  195. Block: TMarkDownCodeBlock;
  196. begin
  197. SetupParser('~~~ pascal'#10'var i: Integer;'#10'~~~');
  198. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  199. Block := GetBlock(0) as TMarkDownCodeBlock;
  200. AssertNotNull('Block should be a code block', Block);
  201. AssertTrue('Should be a fenced code block', Block.Fenced);
  202. AssertEquals('Language info string incorrect', 'pascal', Block.Lang);
  203. end;
  204. { TTestBlockQuotes }
  205. procedure TTestBlockQuotes.TestSimpleQuote;
  206. var
  207. Block: TMarkDownQuoteBlock;
  208. begin
  209. SetupParser('> This is a quote.');
  210. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  211. Block := GetBlock(0) as TMarkDownQuoteBlock;
  212. AssertNotNull('Block should be a quote block', Block);
  213. end;
  214. procedure TTestBlockQuotes.TestNestedQuote;
  215. var
  216. OuterQuote, InnerQuote: TMarkDownQuoteBlock;
  217. begin
  218. SetupParser('> First level'#10'>> Second level');
  219. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  220. AssertEquals('Outer block should be a quote', TMarkDownQuoteBlock,GetBlock(0).ClassType);
  221. OuterQuote :=GetBlock(0) as TMarkDownQuoteBlock;
  222. AssertEquals('Outer quote should have 2 blocks inside', 2, OuterQuote.Blocks.Count); // Para and another quote
  223. AssertEquals('First inner block is a paragraph', TMarkDownParagraphBlock,OuterQuote.Blocks[0].ClassType);
  224. AssertEquals('Second inner block should be a quote', TMarkDownQuoteBlock,OuterQuote.Blocks[1].ClassType);
  225. InnerQuote :=OuterQuote.Blocks[1] as TMarkDownQuoteBlock;
  226. AssertEquals('Outer quote should have 1 block inside', 1, InnerQuote.Blocks.Count); // Para and another quote
  227. AssertEquals('First inner block is a paragraph', TMarkDownParagraphBlock,InnerQuote.Blocks[0].ClassType);
  228. end;
  229. procedure TTestBlockQuotes.TestLazy;
  230. var
  231. OuterQuote: TMarkDownQuoteBlock;
  232. begin
  233. SetupParser('> First level'#10'Continues');
  234. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  235. AssertEquals('Outer block should be a quote', TMarkDownQuoteBlock,GetBlock(0).ClassType);
  236. OuterQuote :=GetBlock(0) as TMarkDownQuoteBlock;
  237. AssertEquals('Outer quote should have 1 blocks inside', 1, OuterQuote.Blocks.Count); // Para and another quote
  238. AssertEquals('First inner block is a paragraph', TMarkDownParagraphBlock,OuterQuote.Blocks[0].ClassType);
  239. end;
  240. { TTestLists }
  241. procedure TTestLists.TestUnorderedList;
  242. var
  243. List: TMarkDownListBlock;
  244. ListItem: TMarkDownListItemBlock;
  245. begin
  246. SetupParser('* Item 1'#10'* Item 2');
  247. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  248. List := GetBlock(0) as TMarkDownListBlock;
  249. AssertNotNull('Block should be a list', List);
  250. AssertFalse('List should be unordered', List.Ordered);
  251. AssertEquals('List should have 2 items', 2, List.Blocks.Count);
  252. // Check first list item and its contents
  253. AssertTrue('First item should be a list item block', List.Blocks[0] is TMarkDownListItemBlock);
  254. ListItem := List.Blocks[0] as TMarkDownListItemBlock;
  255. AssertEquals('First list item should contain one inner block', 1, ListItem.Blocks.Count);
  256. AssertTrue('Inner block of first list item should be a paragraph', ListItem.Blocks[0] is TMarkDownParagraphBlock);
  257. CheckBlockText('First block',ListItem,'Item 1',True);
  258. // Check second list item and its contents
  259. AssertTrue('Second item should be a list item block', List.Blocks[1] is TMarkDownListItemBlock);
  260. ListItem := List.Blocks[1] as TMarkDownListItemBlock;
  261. AssertEquals('Second list item should contain one inner block', 1, ListItem.Blocks.Count);
  262. AssertTrue('Inner block of second list item should be a paragraph', ListItem.Blocks[0] is TMarkDownParagraphBlock);
  263. CheckBlockText('Second block',ListItem,'Item 2',True);
  264. end;
  265. procedure TTestLists.TestOrderedList;
  266. var
  267. List: TMarkDownListBlock;
  268. ListItem: TMarkDownListItemBlock;
  269. begin
  270. SetupParser('1. First item'#10'2. Second item');
  271. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  272. List := GetBlock(0) as TMarkDownListBlock;
  273. AssertNotNull('Block should be a list', List);
  274. AssertTrue('List should be ordered', List.Ordered);
  275. AssertEquals('List should have 2 items', 2, List.Blocks.Count);
  276. ListItem := List.Blocks[0] as TMarkDownListItemBlock;
  277. AssertEquals('First list item should contain one inner block', 1, ListItem.Blocks.Count);
  278. AssertTrue('Inner block of first list item should be a paragraph', ListItem.Blocks[0] is TMarkDownParagraphBlock);
  279. CheckBlockText('First block',ListItem,'First item',True);
  280. ListItem := List.Blocks[1] as TMarkDownListItemBlock;
  281. AssertEquals('Second list item should contain one inner block', 1, ListItem.Blocks.Count);
  282. AssertTrue('Inner block of second list item should be a paragraph', ListItem.Blocks[0] is TMarkDownParagraphBlock);
  283. CheckBlockText('First block',ListItem,'Second item',True);
  284. end;
  285. procedure TTestLists.TestNestedList;
  286. var
  287. OuterList, InnerList: TMarkDownListBlock;
  288. OuterItem: TMarkDownListItemBlock;
  289. begin
  290. SetupParser('* Level 1'#10' * Level 2');
  291. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  292. OuterList := GetBlock(0) as TMarkDownListBlock;
  293. AssertNotNull('Outer block should be a list', OuterList);
  294. AssertEquals('Outer list should have 1 item', 1, OuterList.Blocks.Count);
  295. OuterItem := OuterList.Blocks[0] as TMarkDownListItemBlock;
  296. AssertEquals('Outer item should contain 2 blocks (para, list)', 2, OuterItem.Blocks.Count);
  297. InnerList := OuterItem.Blocks[1] as TMarkDownListBlock;
  298. AssertNotNull('Inner block should be a list', InnerList);
  299. end;
  300. { TTestThematicBreaks }
  301. procedure TTestThematicBreaks.TestAsteriskBreak;
  302. begin
  303. SetupParser('***');
  304. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  305. AssertTrue('Block should be a thematic break', GetBlock(0) is TMarkDownThematicBreakBlock);
  306. end;
  307. procedure TTestThematicBreaks.TestUnderscoreBreak;
  308. begin
  309. SetupParser('---');
  310. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  311. AssertTrue('Block should be a thematic break', GetBlock(0) is TMarkDownThematicBreakBlock);
  312. end;
  313. { TTestTables }
  314. procedure TTestTables.TestSimpleTable;
  315. var
  316. Table: TMarkDownTableBlock;
  317. HeaderRow, BodyRow: TMarkDownTableRowBlock;
  318. begin
  319. SetupParser(
  320. '| Header 1 | Header 2 |'#10 +
  321. '|----------|----------|'#10 +
  322. '| Cell 1 | Cell 2 |'
  323. );
  324. AssertEquals('Document should have 1 block', 1, Doc.Blocks.Count);
  325. Table := GetBlock(0) as TMarkDownTableBlock;
  326. AssertNotNull('Block should be a table', Table);
  327. AssertEquals('Table should have 2 rows', 2, Table.Blocks.Count);
  328. AssertEquals('Table should have 2 columns', 2, Length(Table.Columns));
  329. HeaderRow := Table.Blocks[0] as TMarkDownTableRowBlock;
  330. AssertNotNull('First row should be a table row', HeaderRow);
  331. AssertEquals('Header row should have 2 cells', 2, HeaderRow.Blocks.Count);
  332. CheckTextnodeText('Header row, Cell 1',HeaderRow.Blocks[0],'Header 1');
  333. CheckTextnodeText('Header row, Cell 2',HeaderRow.Blocks[1],'Header 2');
  334. BodyRow := Table.Blocks[1] as TMarkDownTableRowBlock;
  335. AssertNotNull('Second row should be a table row', BodyRow);
  336. AssertEquals('Body row should have 2 cells', 2, BodyRow.Blocks.Count);
  337. CheckTextnodeText('Body Row 1, Cell 1',BodyRow.Blocks[0],'Cell 1');
  338. CheckTextnodeText('Body Row 1, Cell 2',BodyRow.Blocks[1],'Cell 2');
  339. end;
  340. initialization
  341. RegisterTests('Parser',[TTestParagraphs, TTestHeadings, TTestCodeBlocks,
  342. TTestBlockQuotes, TTestLists, TTestThematicBreaks,
  343. TTestTables]);
  344. end.