Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

content: Add start attribute support for ordered list #1329

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

lakshya1goel
Copy link
Contributor

@lakshya1goel lakshya1goel commented Feb 5, 2025

Fixes: #59 and #1356

Screenshot

Before After
WhatsApp Image 2025-02-05 at 8 46 10 PM WhatsApp Image 2025-02-14 at 11 32 54 PM

@gnprice gnprice added the maintainer review PR ready for review by Zulip maintainers label Feb 11, 2025
Copy link
Collaborator

@rajveermalviya rajveermalviya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this @lakshya1goel! Some small comments below, otherwise looks good.

@lakshya1goel
Copy link
Contributor Author

Thanks for the review @rajveermalviya, pushed the revision. PTAL.

Copy link
Collaborator

@rajveermalviya rajveermalviya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the revision @lakshya1goel! Left some small comments, mostly nits.

@lakshya1goel
Copy link
Contributor Author

Pushed the revision, PTAL. Thanks!

Copy link
Collaborator

@rajveermalviya rajveermalviya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the revision @lakshya1goel!
Apart from one comment, this LGTM! Marking for Greg's review.

@rajveermalviya rajveermalviya added integration review Added by maintainers when PR may be ready for integration and removed maintainer review PR ready for review by Zulip maintainers labels Feb 13, 2025
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @lakshya1goel, and thanks @rajveermalviya for the previous reviews!

Generally this looks good. Comments below, most of them small.

Comment on lines 505 to 507
child: Table(
textBaseline: localizedTextBaseline(context),
defaultVerticalAlignment: TableCellVerticalAlignment.baseline,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have an issue in the tracker for the problem this is fixing? Let's file one if not, and then the commit message (and PR) can be marked as fixing it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we didn't have one previously so I have filed #F1356 for it.

@lakshya1goel lakshya1goel force-pushed the issue59 branch 2 times, most recently from 7fb35b3 to 0c6fda4 Compare February 14, 2025 17:11
@lakshya1goel lakshya1goel requested a review from gnprice February 14, 2025 18:16
@lakshya1goel
Copy link
Contributor Author

Thanks for the detailed review @gnprice, I have filed issue #F1356 and mentioned that in commit and PR as well, also pushed the revision resolving all the comments. PTAL!

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the revision! Mostly small comments now.

Comment on lines -1067 to -1064
switch (element.localName) {
case 'ol': listStyle = ListStyle.ordered; break;
case 'ul': listStyle = ListStyle.unordered; break;
}
assert(listStyle != null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should still have an assert for this method's expectations on element.localName.

If you look at other methods on this class, you can see that that's the pattern they follow: any facts they assume (which their callers are expected to ensure), they assert at the top. And then down at the if/else below, this assumption is essential for understanding why the logic makes sense, so it's important to make explicit within the method.

(See also #1329 (comment) — this is why I said there that the references to ListStyle could switch to referring to element.localName directly, instead of saying they'd be deleted.)

Comment on lines 1090 to 1085
} else {
return UnorderedListNode(items: items, debugHtmlNode: debugHtmlNode);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Specifically, this only makes sense because we know that element.localName is "ol" at this point, not some arbitrary other value like "p" or "span" or "div".)

@@ -1155,6 +1158,32 @@ class ContentExample {
], isHeader: false),
]),
]);

static const orderedListLargeStart = ContentExample(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: in general, try to keep test code in the same order as the code it's testing

Here, that means let's order these examples to match the parsing code — so these list examples go above the spoiler examples, just after the many inline examples.

Comment on lines 1545 to 1611
OrderedListNode(
start: 1,
items: [[
QuotationNode([
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: keep the existing formatting:

Suggested change
OrderedListNode(
start: 1,
items: [[
QuotationNode([
OrderedListNode(start: 1, items: [[
QuotationNode([

In particular the version in the current revision of the PR doesn't have correct indentation: the QuotationNode inside items needs to be indented more deeply than the line items starts on.

],
);

static const orderedListCustomStart = ContentExample(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: put this "custom" example before "large" — after all, the "large" value is a custom value too, so it's a particular case of being "custom"

expect(find.text('fourth'), findsOneWidget);
});

testWidgets('list uses correct text baseline alignment', (tester) async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess this is good to check too but it's not the thing I was hoping for in #1329 (comment) — it doesn't check that #1356 was (and remains) fixed.

In particular if I make this edit:

--- lib/widgets/content.dart
+++ lib/widgets/content.dart
@@ -507,7 +507,7 @@ class ListNodeWidget extends StatelessWidget {
         defaultVerticalAlignment: TableCellVerticalAlignment.baseline,
         textBaseline: localizedTextBaseline(context),
         columnWidths: const <int, TableColumnWidth>{
-          0: IntrinsicColumnWidth(),
+          0: FixedColumnWidth(20),
           1: FlexColumnWidth(),
         },

then that should reintroduce #1356 — it'd make the new code behave pretty much just like the old code before your fix — but there isn't currently a test that would detect that and fail.

Can you find a way to write a test that checks that #1356 is fixed?

Comment on lines 250 to 256
await prepareContent(tester, Directionality(
textDirection: TextDirection.rtl,
child: plainContent(ContentExample.orderedListLargeStart.html)));

final tableRtl = tester.widget<Table>(find.byType(Table));
check(tableRtl.defaultVerticalAlignment).equals(TableCellVerticalAlignment.baseline);
check(tableRtl.textBaseline).equals(localizedTextBaseline(tester.element(find.byType(Table))));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the baseline logic interact with the text direction?

I think it doesn't, and it doesn't seem likely that it would. So let's leave this part of the test out, as being redundant.

@lakshya1goel
Copy link
Contributor Author

Pushed the revision @gnprice, PTAL. Thanks!

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the revision! Comments below.

Comment on lines 276 to 280
[OrderedListNode(start: 5, items: [
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('fifth')])],
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('sixth')])],
])
],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: two-space indent; and the two [-enclosed lists start on the same line, so the closing ] can go on the same line:

Suggested change
[OrderedListNode(start: 5, items: [
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('fifth')])],
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('sixth')])],
])
],
[OrderedListNode(start: 5, items: [
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('fifth')])],
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('sixth')])],
])],

Comment on lines 1518 to 1519
testParseExample(ContentExample.orderedListCustomStart);
testParseExample(ContentExample.orderedListLargeStart);
testParseExample(ContentExample.spoilerDefaultHeader);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use blank lines to group things logically:

Suggested change
testParseExample(ContentExample.orderedListCustomStart);
testParseExample(ContentExample.orderedListLargeStart);
testParseExample(ContentExample.spoilerDefaultHeader);
testParseExample(ContentExample.orderedListCustomStart);
testParseExample(ContentExample.orderedListLargeStart);
testParseExample(ContentExample.spoilerDefaultHeader);

(including the existing blank line that was there between the multi-line group block and these function calls)

Comment on lines 1518 to 1519
testParseExample(ContentExample.orderedListCustomStart);
testParseExample(ContentExample.orderedListLargeStart);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh and speaking of grouping logically: these should go inside the "parse lists" group (just above)

Comment on lines 255 to 257
find.descendant(of: find.byType(Align), matching: find.byType(Text)));

for (final text in markerTexts) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: avoid trailing whitespace at end of line

Suggested change
find.descendant(of: find.byType(Align), matching: find.byType(Text)));
for (final text in markerTexts) {
find.descendant(of: find.byType(Align), matching: find.byType(Text)));
for (final text in markerTexts) {

If you read your changes with git log -p, it should highlight this for you. In my terminal it looks like this:
image

(I recommend regularly reading Git commits — yours and other people's — with git log --stat -p, and using this "secret" for convenient navigation between commits.)

Comment on lines 258 to 261
final renderParagraph = tester.renderObject(find.text(text.data!)) as RenderParagraph;
final textHeight = renderParagraph.size.height;
final lineHeight = renderParagraph.text.style!.height! * renderParagraph.text.style!.fontSize!;
check(textHeight).equals(lineHeight);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good strategy. Let's add one other check:

Suggested change
final renderParagraph = tester.renderObject(find.text(text.data!)) as RenderParagraph;
final textHeight = renderParagraph.size.height;
final lineHeight = renderParagraph.text.style!.height! * renderParagraph.text.style!.fontSize!;
check(textHeight).equals(lineHeight);
final renderParagraph = tester.renderObject(find.text(text.data!)) as RenderParagraph;
final textHeight = renderParagraph.size.height;
final lineHeight = renderParagraph.text.style!.height! * renderParagraph.text.style!.fontSize!;
check(textHeight).equals(lineHeight);
check(renderParagraph.didExceedMaxLines).isFalse();

That way if we had a maxLines: 1 on that Text widget (very similar to how web behaved for years until the recent fix), that would break the test too.

Comment on lines 254 to 255
final markerTexts = tester.widgetList<Text>(
find.descendant(of: find.byType(Align), matching: find.byType(Text)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking for Align here makes this tied into some fairly internal details of how ListNodeWidget is implemented.

Instead, let's use find.textContaining('9999'). That should find the same Text widget in a way that's more focused on what the user sees, rather than how the implementation works. See:
https://zulip.readthedocs.io/en/latest/testing/philosophy.html#integration-testing-or-unit-testing

(It does make this test dependent on the details of the test data it's using, from ContentExample.orderedListLargeStart. That's OK — that test data exists basically for the sake of this test and the related parsing test.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we only need to check one marker, not both.

Comment on lines 263 to 267
final textWidth = renderParagraph.size.width;
final renderBox = tester.renderObject(find.ancestor(
of: find.text(text.data!),
matching: find.byType(Align))) as RenderBox;
check(renderBox.size.width >= textWidth).isTrue();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check can't really fail — it's checking that the parent is at least as big as the child, and that's always true unless one uses some fairly special widgets that aren't likely to get involved here. We can leave it out.

Comment on lines 269 to 271
final align = tester.widget<Align>(
find.ancestor(of: find.text(text.data!), matching: find.byType(Align)));
check(align.alignment).equals(AlignmentDirectional.topEnd);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's adapt this so that it's in terms of what the user sees, rather than how the implementation code works. (Per https://zulip.readthedocs.io/en/latest/testing/philosophy.html#integration-testing-or-unit-testing again.)

Concretely: the content in this test has two markers. They aren't going to be the same width. So if the alignment is correct, they'll have their right edges at the same x-coordinate, and if the alignment is wrong they'll almost surely have their right edges at different x-coordinates. So to write a test, get the locations of the two marker Texts, confirm the widths are different, and then check the right edges match.

check(table.textBaseline).equals(localizedTextBaseline(tester.element(find.byType(Table))));
});

testWidgets('long ordered list markers render completely and are right-aligned', (tester) async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this should be two test cases (two testWidgets calls) — one for having enough room, and one for being end-aligned

(also it's not right-aligned in general — it's end-aligned, which means the left side when using an RTL language, like Persian or Arabic or Hebrew)

(it's fine not to add a test for the RTL case, just adjust this test name so it's accurate)

@@ -251,21 +251,11 @@ class HeadingNode extends BlockInlineContainerNode {
}
}

enum ListStyle { ordered, unordered }
sealed class ListNode extends BlockContentNode {
const ListNode({required this.items, super.debugHtmlNode});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I missed this earlier — let's keep the constructor calls a bit simpler:

Suggested change
const ListNode({required this.items, super.debugHtmlNode});
const ListNode(this.items, {super.debugHtmlNode});

(Just like in the existing version, and like in some other classes in this file: QuotationNode, CodeBlockNode, TextNode, and others where it's clear what a positional argument should mean.)

@lakshya1goel
Copy link
Contributor Author

lakshya1goel commented Feb 22, 2025

Thanks for the detailed review @gnprice , I have pushed the revision, PTAL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Handle <ol start=…>
3 participants