Pārlūkot izejas kodu

* Correct nested list processing

Michaël Van Canneyt 2 nedēļas atpakaļ
vecāks
revīzija
404d27b667

+ 25 - 24
packages/fcl-md/src/markdown.parser.pas

@@ -52,12 +52,11 @@ type
   protected
     function inListOrQuote : boolean; virtual;
     // Access to parser methods
-    function isList(ordered : boolean; const marker : String; indent : integer) : boolean; virtual;
     function PeekLine : TMarkDownLine;
     function NextLine : TMarkDownLine;
     function Done : Boolean;
     procedure RedoLine(aResetLine: Boolean);
-    function InList(blocks : TMarkDownBlockList; ordered : boolean; marker : String; indent : integer; grace : integer; out list : TMarkDownListBlock) : boolean;
+    function InList(aBlock : TMarkDownBlock; ordered : boolean; marker : String; indent : integer; grace : integer; out list : TMarkDownListBlock) : boolean;
     function IsBlock(aBlock : TMarkDownBlock; blocks : TMarkDownBlockList; const aLine : String; wsLen : integer = 3) : boolean;
     function CurrentLine : TMarkDownLine;
     procedure Parse(aParent: TMarkDownContainerBlock; aPArentProcessor: TMarkDownBlockProcessor); overload;
@@ -129,7 +128,7 @@ type
     // status
     // Is the last block a list with the given properties ?
     // if yes, return the list
-    function InList(aBlocks: TMarkDownBlockList; aOrdered: boolean; const aMarker: String; aIndent: integer; aGrace: integer; out
+    function InList(aBlock: TMarkDownBlock; aOrdered: boolean; const aMarker: String; aIndent: integer; aGrace: integer; out
       aList: TMarkDownListBlock): boolean;
     // Does aLine start a new block (true) or can it be a continuation (false) ?
     function IsBlock(aParent: TMarkDownBlock; aBlocks: TMarkDownBlockList; const aLine: String; aWhiteSpaceLen: integer): boolean;
@@ -269,11 +268,11 @@ begin
 end;
 
 
-function TMarkDownBlockProcessor.InList(blocks: TMarkDownBlockList; ordered: boolean; marker: String; indent: integer;
-  grace: integer; out list: TMarkDownListBlock): boolean;
+function TMarkDownBlockProcessor.InList(aBlock: TMarkDownBlock; ordered: boolean; marker: String; indent: integer; grace: integer;
+  out list: TMarkDownListBlock): boolean;
 
 begin
-  Result:=FParser.InList(blocks,ordered,marker,indent,grace,list);
+  Result:=FParser.InList(aBlock,ordered,marker,indent,grace,list);
 end;
 
 
@@ -305,14 +304,6 @@ begin
   Result:=FParser.ParentProcessor;
 end;
 
-function TMarkDownBlockProcessor.isList(ordered: boolean; const marker: String; indent: integer): boolean;
-
-begin
-  Result:=false;
-  if ordered and (marker<>'') and (indent>0) then ; // keep compiler happy
-end;
-
-
 { TMarkDownDocumentProcessor }
 
 function TMarkDownDocumentProcessor.processLine(aParent : TMarkDownContainerBlock; aLine: TMarkDownLine; aContext : TMarkDownBlockProcessingContext): Boolean;
@@ -496,17 +487,27 @@ begin
 end;
 
 
-function TMarkDownParser.InList(aBlocks: TMarkDownBlockList; aOrdered: boolean; const aMarker: String; aIndent: integer; aGrace: integer; out aList: TMarkDownListBlock): boolean;
-
+function TMarkDownParser.InList(aBlock: TMarkDownBlock; aOrdered: boolean; const aMarker: String; aIndent: integer;
+  aGrace: integer; out aList: TMarkDownListBlock): boolean;
+var
+  lBlock: TMarkDownBlock;
+  lList : TMarkDownListBlock absolute lBlock;
 begin
-  Result:=(aBlocks.Count > 0) and (aBlocks.Last is TMarkDownListBlock);
-  if Not Result then
-    exit;
-  aList:=aBlocks.Last as TMarkDownListBlock;
-  Result:=(aList.ordered=aOrdered)
-          and (aList.Marker=aMarker)
-          and (aIndent-aGrace<=aList.LastIndent)
-          and not aList.closed
+  Result:=False;
+  lBlock:=aBlock;
+  While (Not Result) and Assigned(lBlock) do
+    begin
+    Result:=lBlock is TMarkDownListBlock;
+    // Check for exact match: same type, marker, and base indentation level
+    if Result then
+      Result:=(lList.ordered=aOrdered)
+              and (lList.Marker=aMarker)
+              and (aIndent-aGrace <= lList.BaseIndent)
+              and not lList.closed;
+    if Result then
+      aList:=lList;
+    lBlock:=lBlock.Parent;
+    end;
 end;
 
 

+ 101 - 123
packages/fcl-md/src/markdown.processors.pas

@@ -57,10 +57,7 @@ type
     FEmptyLine : integer;
     FLastList : TMarkDownListBlock;
     FLastItem : TMarkDownListItemBlock;
-    FIndent : Integer;
-    FMarker : String;
   protected
-    function isList(aOrdered : boolean; const aMarker : String; aIndent : integer) : boolean; override;
     function inListOrQuote : boolean; override;
     function Root : Boolean;
     function LastList : TMarkDownListBlock;
@@ -72,6 +69,7 @@ type
 
   TUListProcessor = class (TMarkDownListProcessor)
   private
+    function HasMarker(aLine: TMarkDownLine; out aIndent: Integer; out aMarker: String): boolean;
     function IsItemInList(aList: TMarkDownListBlock; aLine: TMarkDownLine): boolean;
   public
     function LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean; override;
@@ -84,6 +82,7 @@ type
   TOListProcessor = class (TMarkDownListProcessor)
   private
     FStart : Integer;
+    function HasMarker(aLine: TMarkDownLine; out aIndent: integer; out aMarker: String; out aValue: Integer): boolean;
     function IsItemInList(aList: TMarkDownListBlock; aLine: TMarkDownLine): boolean;
   Public
     function LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean; override;
@@ -186,7 +185,7 @@ begin
   if Len=0 then
     Exit(False);
   inQuote:=(Len=FLevel);
-//  InQuote:=StartsWithWhiteSpace(aLine.Remainder,'>',len);
+  //  InQuote:=StartsWithWhiteSpace(aLine.Remainder,'>',len);
   // Not enough markers -> check continuation.
   // end of block if:
   // - empty line
@@ -305,18 +304,6 @@ begin
 end;
 
 
-function TMarkDownListProcessor.isList(aOrdered: boolean; const aMarker: String; aIndent: integer): boolean;
-
-begin
-  Result:=Assigned(FLastList);
-  if Result then
-    begin
-    Result:=(FLastList.Ordered=aOrdered)
-            and (FLastList.Marker=aMarker)
-            and (aIndent<FLastList.lastIndent + 1);
-    end;
-end;
-
 function TMarkDownListProcessor.prepareLine(aLine: TMarkDownLine; aContext : TMarkDownBlockProcessingContext): boolean;
 
 var
@@ -327,9 +314,8 @@ begin
   lCurrLineNo:=aLine.LineNo;
   lLastIndent:=LastList.LastIndent;
   lWhiteSpace:=aLine.LeadingWhitespace;
-  if lCurrLineNo=FLastItem.Line then
-    aLine.advance(lLastIndent)
-  else if lWhitespace >= lLastIndent then
+
+  if lWhitespace >= lLastIndent then
     begin
     len:=lWhitespace;
     if (len>lLastIndent+1) then
@@ -430,51 +416,40 @@ begin
   if (lBlock is TMarkDownListBlock) then
     if aLine.LeadingWhitespace>=lList.LastIndent then
       begin
-      aLine.Advance(lList.LastIndent);
+//      aLine.Advance(lList.LastIndent);
       Result:=False;
       end;
 end;
 
-function TUListProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
-
+function TUListProcessor.HasMarker(aLine: TMarkDownLine; Out aIndent : Integer; out aMarker : String): boolean;
 var
-  ws,i, i2 : integer;
-  s : String;
-  list : TMarkDownListBlock;
+  lMarker : string;
+  lIndent : Integer;
 
 begin
   Result:=False;
   if aLine.isEmpty then
     Exit;
-  ws:=aLine.LeadingWhitespace;
-  if ws>3 then
-    begin
-    FMarker:=CopySkipped(aLine.Remainder,[' ',#9]);
-    if (FMarker='') or not inList(aParent.blocks, false, FMarker[1], ws, 1, list) then
-      Exit;
-    end;
-  i:=aLine.LeadingWhitespace;
-  s:=aLine.Remainder;
-  if (Length(s)<1+i) then
-    Exit;
-  if not CharInSet(s[1+i],['+','-','*']) then
+  lIndent:=aLine.LeadingWhitespace;
+  lMarker:=CopySkipped(aLine.Remainder,[' ',#9]);
+  if (lMarker='')  then
     Exit;
-  FMarker:=s[1+i];
-  s:=Copy(S,2+i,Length(S)-i);
-  if isWhitespace(s) and Parser.inPara(aParent.blocks, false) then
+  if not CharInSet(lMarker[1],['+','-','*']) then
     Exit;
-  if isWhitespace(s) then // nothing after if it's the only thing on the aLine
-    i2:=1
-  else
-    begin
-    i2:=LeadingWhitespace(s);
-    if (i2 = 0) then
-      Exit;
-    if (i2 >= 5) then
-      i2:=1;
-    end;
-  FIndent:=i+i2+1;
-  Result:=True;
+  if (lMarker[2]<>' ') then
+    exit;
+  aMarker:=lMarker[1];
+  aIndent:=lIndent;
+  Result:=true;
+end;
+
+function TUListProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+
+var
+  lMarker : string;
+  lIndent : integer;
+begin
+  Result:=HasMarker(aLine,lIndent,lMarker);
 end;
 
 function TUListProcessor.IsItemInList(aList : TMarkDownListBlock; aLine : TMarkDownLine) : boolean;
@@ -487,6 +462,8 @@ begin
   if Not Result then
     exit;
   Result:=StartsWithWhitespace(aLine.line,aList.marker[1],len,aList.baseIndent);
+  if Result then
+    Result := (len >= aList.baseIndent);
 end;
 
 function TUListProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine; aContext: TMarkDownBlockProcessingContext): boolean;
@@ -494,21 +471,27 @@ function TUListProcessor.processLine(aParent: TMarkDownContainerBlock; aLine: TM
 var
   lOldLastList, lList : TMarkDownListBlock;
   lOldLastItem, lItem : TMarkDownListItemBlock;
-  lMarker : string;
+  lRemain,lMarker : string;
+  lIndent : Integer;
   lNewItem : Boolean;
 
 begin
+  Result:=False;
+  lRemain:=aLine.Remainder;
+  if not HasMarker(aLine,lIndent,lMarker) then exit;
+
+  aLine.Advance(Pos(lMarker,aLine.Remainder)+1);
+  lRemain:=aLine.Remainder;
   lOldLastList:=FLastList;
   lOldLastItem:=FLastItem;
-  lMarker:=FMarker;
-  if InList(aParent.blocks,False,lMarker,FIndent,1,lList) then
-    lList.Lastindent:=FIndent
+  if InList(aParent,False,lMarker,lIndent,1,lList) then
+    lList.Lastindent:=lIndent
   else
     begin
     lList:=TMarkDownListBlock.Create(aParent,aLine.LineNo);
     lList.Ordered:=false;
-    lList.BaseIndent:=FIndent;
-    lList.LastIndent:=lList.BaseIndent;
+    lList.BaseIndent:=lIndent;
+    lList.LastIndent:=lIndent;
     lList.Marker:=lMarker;
     FLastList:=lList;
     end;
@@ -522,8 +505,11 @@ begin
     lNewItem:=Not Done;
     if lNewItem then
       begin
-      aLine:=NextLine;
+      aLine:=PeekLine;
+      // Check if next line is a list item at the SAME level
       lNewItem:=IsItemInList(lList,aLine);
+      if lnewItem then
+        aLine:=NextLine;
       end;
     lItem.Closed:=true;
   until not lNewItem;
@@ -537,61 +523,39 @@ end;
   TOListProcessor
   ---------------------------------------------------------------------}
 
-function TOListProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
-
+function TOListProcessor.HasMarker(aLine: TMarkDownLine; out aIndent : integer; out aMarker : String; out aValue : Integer): boolean;
 var
-  list : TMarkDownListBlock;
-  lMarkerLen,lIndent, i2 : integer;
-  lMarker,lLine : String;
+  lValueLen,lIndent : integer;
+  lLine,lValue,lMarker : string;
 
 begin
-  Result:=false;
-  if aLine.isEmpty then
-    Exit;
-  if (aLine.LeadingWhitespace >= 4) then
-    begin
-    FMarker:=CopySkipped(aLine.Remainder, [' ']);
-    FMarker:=CopySkipped(FMarker, ['0'..'9']);
-    if (FMarker = '') or not inList(aParent.blocks, true, FMarker[1], aLine.LeadingWhitespace, 2, list) then
-      Exit;
-    end;
+  Result:=False;
   lIndent:=aLine.LeadingWhitespace;
-  aLine.mark;
-  try
-    aLine.SkipWhiteSpace;
-    lLine:=aLine.Remainder;
-    lMarker:=CopyMatching(lLine, ['0'..'9']);
-    lMarkerLen:=length(lMarker)+1; // ending dot or )
-    if (lMarkerLen=1) or (lMarkerLen>10) or (lMarkerLen>Length(lLine)) then
-      Exit;
-    if not CharInSet(lLine[lMarkerLen], ['.', ')']) then
-      Exit;
-    // rule 267
-    if Parser.inPara(aParent.blocks, false) and (lMarker<>'1') then
-      Exit;
-    FMarker:=lLine[lMarkerLen];
-    FStart:=StrToIntDef(lMarker,1);
-    inc(lIndent,lMarkerLen);
-    lLine:=Copy(lLine,lMarkerLen+1,Length(lLine)-lMarkerLen);
-    if isWhitespace(lLine) and Parser.inPara(aParent.blocks, false) then
-      Exit;
-    Result:=true;
-  finally
-    if not Result then
-      aLine.rewind;
-  end;
-  // Calculate indent to use...
+  lLine:=aLine.Remainder;
+  lLine:=CopySkipped(lLine,[' ',#9]);
+  lValue:=CopyMatching(lLine, ['0'..'9']);
+  lValueLen:=length(lValue); // ending dot or )
+  if (lValueLen=0) or (lValueLen>9) or (lValueLen>=Length(lLine)) then
+    Exit;
+  lMarker:=lLine[lValueLen+1];
+  if not CharInSet(LMarker[1], ['.', ')']) then
+    Exit;
+  Delete(lLine,1,lValueLen+1);
   if isWhitespace(lLine) then
-    i2:=1
-  else
-    begin
-    i2:=LeadingWhitespace(lLine);
-    if (i2 = 0) then
-      Exit(false);
-    if (i2 >= 5) then
-      i2:=1;
-    end;
-  FIndent:=lIndent+i2;
+    Exit;
+  aValue:=StrToIntDef(lValue,1);
+  aIndent:=lIndent;
+  aMarker:=lMarker;
+  Result:=True;
+end;
+
+function TOListProcessor.HandlesLine(aParent: TMarkDownContainerBlock; aLine: TMarkDownLine): boolean;
+
+var
+  lIndent,lStart : integer;
+  lMarker : String;
+begin
+  Result:=HasMarker(aLine,lIndent,lMarker,lStart);
 end;
 
 function TOListProcessor.IsItemInList(aList : TMarkDownListBlock; aLine : TMarkDownLine) : boolean;
@@ -607,7 +571,8 @@ begin
   Result:=StartsWithWhitespace(aLine.Line,['0'..'9'],len,aList.baseIndent);
   if Not Result then
     exit;
-  if len<=aList.baseIndent then
+  // Ensure indentation is exactly at the expected level for this list
+  if len = aList.baseIndent then
     begin
     lLine:=aLine.Line;
     if Len>0 then
@@ -616,7 +581,9 @@ begin
     Result:=(lLine<>'') and (aList.marker=lLine[1]);
     if Result then
       Result:=(Length(lLine)>1) and (lLine[2]=' ');
-    end;
+    end
+  else
+    Result:=False;
 end;
 
 function TOListProcessor.LineEndsBlock(aBlock: TMarkDownContainerBlock; aLine: TMarkDownLine): Boolean;
@@ -631,12 +598,14 @@ begin
     Exit;
   if (lBlock is TMarkDownListItemBlock) and (lBlock.Parent is TMarkDownListBlock) then
      lBlock:=lBlock.Parent as TMarkDownListBlock;
-  if (lBlock is TMarkDownListBlock) then
-    if aLine.LeadingWhitespace>=lList.baseIndent then
-      begin
-      aLine.Advance(lList.LastIndent);
-      Result:=False
-      end;
+  if not (lBlock is TMarkDownListBlock) then
+    Exit;
+  // Check if we're still in the parent list
+  if aLine.LeadingWhitespace>=lList.baseIndent then
+    begin
+    aLine.Advance(lList.LastIndent);
+    Result:=False
+    end;
 end;
 
 
@@ -645,20 +614,26 @@ var
   lOldLastList, lList : TMarkDownListBlock;
   lOldLastItem, lItem : TMarkDownListItemBlock;
   lNewItem : Boolean;
+  lMarker : String;
+  lStart,lIndent : Integer;
 begin
+  Result:=False;
+  if not HasMarker(aLine,lIndent,lMarker,lStart) then
+    exit;
+  aLine.Advance(Pos(lMarker,aLine.Remainder)+1);
   lOldLastList:=FLastList;
-  if inList(aParent.blocks, true, FMarker, Findent, 2, lList) then
+  if inList(aParent, true, lMarker, lindent, 2, lList) then
     begin
-    lList.lastIndent:=FIndent;
+    lList.lastIndent:=lIndent;
     FLastList:=lList;
     end
   else
     begin
     lList:=TMarkDownListBlock.Create(aParent,aLine.LineNo);
     lList.ordered:=true;
-    lList.baseIndent:=Findent;
-    lList.lastIndent:=lList.baseIndent;
-    lList.marker:=FMarker;
+    lList.baseIndent:=lindent;
+    lList.lastIndent:=lIndent;
+    lList.marker:=lMarker;
     lList.Start:=FStart;
     FLastList:=lList;
     end;
@@ -673,8 +648,11 @@ begin
     lNewItem:=Not Done;
     if lNewItem then
       begin
-      aLine:=NextLine;
+      aLine:=PeekLine;
+      // Check if next line is a list item at the SAME level
       lNewItem:=IsItemInList(lList,aLine);
+      if lNewItem then
+        aLine:=NextLine;
       end;
   until not lNewItem;
   FLastItem:=lOldLastItem;

+ 185 - 0
packages/fcl-md/tests/utest.markdown.parser.pas

@@ -74,10 +74,17 @@ type
 
   { TTestLists }
   TTestLists = class(TBlockTestCase)
+  private
+    function TestList(const Msg, Source: String; aListCount, aListItemCount: Integer): TMarkDownListBlock;
   published
     procedure TestUnorderedList;
     procedure TestOrderedList;
     procedure TestNestedList;
+    procedure TestNestedList2;
+    procedure TestNestedList3;
+    procedure TestNestedList4;
+    procedure TestNestedList5;
+    procedure TestNestedList6;
   end;
 
   { TTestThematicBreaks }
@@ -363,6 +370,184 @@ begin
   AssertNotNull('Inner block should be a list', InnerList);
 end;
 
+function TTestLists.TestList(const Msg,Source : String; aListCount,aListItemCount:  Integer) : TMarkDownListBlock;
+
+begin
+  SetupParser(Source);
+  AssertEquals(Msg+': Block count',aListCount,FDoc.Blocks.Count);
+  AssertTrue(Msg+': First block is list',FDoc.Blocks[0] is TMarkDownListBlock);
+  Result:=FDoc.Blocks[0] as TMarkDownListBlock;
+  AssertEquals(Msg+': First list item count',aListItemCount,Result.Blocks.Count);
+
+end;
+
+procedure TTestLists.TestNestedList2;
+
+var
+  OuterList, InnerList: TMarkDownListBlock;
+  OuterItem1, OuterItem2: TMarkDownListItemBlock;
+
+begin
+  //
+  OuterList:=TestList(
+    'Basic nested unordered list',
+    '* First item'#10 +
+    '  * Sub item 1'#10 +
+    '  * Sub item 2'#10 +
+    '* Second item',
+    1,2
+  );
+  // Should have 1 top-level list
+  // Outer list should have 2 items
+  if OuterList.Blocks.Count <> 2 then
+    Fail('Outer list block count not 2');
+  if not (OuterList.Blocks[0] is TMarkDownListItemBlock) then
+    Fail('Outer list block 0 not item');
+  if not (OuterList.Blocks[1] is TMarkDownListItemBlock) then
+    Fail('Outer list block 1 not item');
+
+  OuterItem1 := OuterList.Blocks[0] as TMarkDownListItemBlock;
+  OuterItem2 := OuterList.Blocks[1] as TMarkDownListItemBlock;
+
+  // First outer item should contain paragraph + nested list
+  if OuterItem1.Blocks.Count <> 2 then
+    Fail('Item 1 block count not 2');
+  if not (OuterItem1.Blocks[0] is TMarkDownParagraphBlock) then
+    Fail('Item 1 block 0 not paragraph');
+  if not (OuterItem1.Blocks[1] is TMarkDownListBlock) then
+    Fail('Item 1 block 1 not list');
+
+  InnerList := OuterItem1.Blocks[1] as TMarkDownListBlock;
+
+    // Inner list should have 2 items
+  if InnerList.Blocks.Count <> 2 then
+    Fail('Item 1 - Inner list block count not 2');
+  // Second outer item should contain only a paragraph
+  if OuterItem2.Blocks.Count <> 1 then
+    Fail('Item Inner list block item count not 1');
+  if not (OuterItem2.Blocks[0] is TMarkDownParagraphBlock) then
+    Fail('Item Inner list block item content not paragraph ');
+
+end;
+
+procedure TTestLists.TestNestedList3;
+var
+  List1, List2, List3: TMarkDownListBlock;
+  Item1, Item2: TMarkDownListItemBlock;
+begin
+  // Test 2: Deep nesting (3 levels)
+  List1:=TestList(
+    'Deep nested list (3 levels)',
+    '* Level 1'#10 +
+    '  * Level 2'#10 +
+    '    * Level 3',
+    1,1
+  );
+  if List1.Blocks.Count <> 1 then
+    Fail('List 1 must have 1 item');
+
+  Item1 := List1.Blocks[0] as TMarkDownListItemBlock;
+  if Item1.Blocks.Count <> 2 then
+    Fail('List 1 item 1 has 2 blocks');
+
+  List2 := Item1.Blocks[1] as TMarkDownListBlock;
+  if List2.Blocks.Count <> 1 then
+    Fail('List 1 item 1 has 1 list sub');
+
+  Item2 := List2.Blocks[0] as TMarkDownListItemBlock;
+  if Item2.Blocks.Count <> 2 then
+    // paragraph + nested list
+    Fail('List 2 item 1 has 2 blocks');
+
+  List3 := Item2.Blocks[1] as TMarkDownListBlock;
+  if List3.Blocks.Count <> 1 then // deepest level has 1 item
+    Fail('List 3 item 1 has 1 block');
+
+end;
+
+procedure TTestLists.TestNestedList4;
+var
+  OuterList, InnerList: TMarkDownListBlock;
+  OuterItem: TMarkDownListItemBlock;
+begin
+  // Test 3: Ordered nested list
+  OuterList:=TestList(
+    'Ordered nested list',
+    '1. First item'#10 +
+    '   1. Sub item 1'#10 +
+    '   2. Sub item 2'#10 +
+    '2. Second item',
+    1,2
+  );
+  if Not OuterList.Ordered then
+    Fail('Outer must be ordered');
+  if OuterList.Blocks.Count <> 2 then
+    Fail('Outer has 2 items');
+
+  OuterItem := OuterList.Blocks[0] as TMarkDownListItemBlock;
+  if OuterItem.Blocks.Count <> 2 then
+    Fail('Outer item 1 has 2 children');
+
+  InnerList := OuterItem.Blocks[1] as TMarkDownListBlock;
+  if not InnerList.Ordered then
+    Fail('Inner is unordered');
+
+  if InnerList.Blocks.Count <> 2 then
+    Fail('Inner list has 2 items');
+
+end;
+
+procedure TTestLists.TestNestedList5;
+var
+  OuterList, InnerList: TMarkDownListBlock;
+  OuterItem: TMarkDownListItemBlock;
+begin
+  // Test 4: Mixed nesting (unordered containing ordered)
+  OuterList:=TestList(
+    'Mixed nested list (unordered -> ordered)',
+    '* First item'#10 +
+    '  1. Sub item 1'#10 +
+    '  2. Sub item 2',
+    1,1
+  );
+  if OuterList.Ordered then
+    Fail('Outer list must be unordered');
+  if OuterList.Blocks.Count <> 1 then
+    Fail('Outer list must have 1 item');
+  OuterItem := OuterList.Blocks[0] as TMarkDownListItemBlock;
+  if OuterItem.Blocks.Count <> 2 then
+    Fail('Outer item must have 2 children');
+
+  InnerList := OuterItem.Blocks[1] as TMarkDownListBlock;
+  if not InnerList.Ordered then
+    Fail('Inner list must be ordered');
+  if InnerList.Blocks.Count <> 2 then
+    Fail('Inner list must have 2 items');
+end;
+
+procedure TTestLists.TestNestedList6;
+var
+  OuterList, InnerList: TMarkDownListBlock;
+  OuterItem: TMarkDownListItemBlock;
+begin
+   OuterList:=TestList(
+     'Multiple consecutive nested items',
+     '* First item'#10 +
+     '  * Sub item 1'#10 +
+     '  * Sub item 2'#10 +
+     '  * Sub item 3'#10 +
+     '  * Sub item 4'#10 +
+     '* Second item',
+     1,2
+   );
+  OuterItem := OuterList.Blocks[0] as TMarkDownListItemBlock;
+  if OuterItem.Blocks.Count <> 2 then
+    Fail('Outer item should have 2 children');
+  InnerList := OuterItem.Blocks[1] as TMarkDownListBlock;
+  if not InnerList.Blocks.Count = 4 then
+    Fail('Inner list item should have 4 items');
+end;
+
 { TTestThematicBreaks }
 
 procedure TTestThematicBreaks.TestAsteriskBreak;