Skip to content

content: Add start attribute support for ordered list #1329

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

Merged
merged 3 commits into from
Mar 3, 2025

Conversation

lakshya1goel
Copy link
Contributor

@lakshya1goel lakshya1goel commented Feb 5, 2025

Fixes: #59
Fixes: #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
Member

@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
Member

@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
Member

@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 517
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 -1116
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 1722
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 1624
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.

@gnprice
Copy link
Member

gnprice commented Feb 24, 2025

(I've just edited the PR description so that it's marked as fixing #1356 too — the GitHub "fixes" syntax doesn't understand "and", and needs the word "fixes" to be right next to the issue number.)

@gnprice gnprice mentioned this pull request Feb 24, 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 for the revision! Just a couple of small comments now.

Comment on lines 264 to 271
final marker9999 = tester.renderObject(find.textContaining('9999.')) as RenderParagraph;
final marker10000 = tester.renderObject(find.textContaining('10000.')) as RenderParagraph;
check(marker9999.size.width != marker10000.size.width).isTrue();

final marker9999Pos = marker9999.localToGlobal(Offset.zero);
final marker10000Pos = marker10000.localToGlobal(Offset.zero);
check(marker9999Pos.dx + marker9999.size.width)
.equals(marker10000Pos.dx + marker10000.size.width);
Copy link
Member

Choose a reason for hiding this comment

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

Cool, this test is accurate, but can be simplified using getRect:

      final marker9999 = tester.getRect(find.textContaining('9999.'));

then marker9999.width and marker9999.right.

Copy link
Member

Choose a reason for hiding this comment

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

(If you dig into the implementation of getRect, you'll see it's doing something similar to this test's logic.)

])]
])],
])],
], start: 1)],
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 start: 1 as the first argument, before the positional items argument

Dart has the handy feature of allowing named and positional arguments to be interleaved in any order, including named first. That's useful to do in a case like this where there's a named argument that's short and fits easily on the first line and then a longer multi-line argument that's positional.

@lakshya1goel
Copy link
Contributor Author

Pushed the revision, Please have a look @gnprice . Thanks!

lakshya1goel and others added 3 commits March 3, 2025 13:42
…in a bit more

A couple of the extension members added here don't actually end up
getting used (because the "height of one line" check feels like it
benefits from describing the calculation more explicitly).  Still
the fact we're referring to those properties in a test makes a good
prompt to add the extension so future tests can use it.
@gnprice
Copy link
Member

gnprice commented Mar 3, 2025

Thanks! Looks good; merging.

I made a few tweaks in the first commit along the lines of #1329 (comment), for two-space indentation and to avoid other formatting changes. Also added one commit on top which I recommend reading:
e0ee70e content test [nfc]: Use checks-extensions in list-marker tests; explain a bit more

since it points to an important bit of test-writing style which I'd missed in previous reviews.

In particular, a change like this:

-      check(marker9999.right).equals(marker10000.right);
+      check(marker9999).right.equals(marker10000.right);

means that if that check fails, the resulting error will be more informative because it will have the whole Rect value marker9999 rather than just the one piece of it that is marker9999.right. To see the difference, try sabotaging the test to fail and then trying both versions, with .right inside and outside the parens, to see the output.

@gnprice gnprice merged commit e0ee70e into zulip:main Mar 3, 2025
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.

Fix ordered list layout to handle long numbers in <ol> Handle <ol start=…>
3 participants