-
Notifications
You must be signed in to change notification settings - Fork 45
feat: Support nested inline footnotes #617
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
Conversation
a93627c
to
94dc4c8
Compare
That sounds like a good idea. |
Dunno if it's a good idea :) adding complexity, nesting, the risk of infinite loops or worse layout... to something already quite fragile, for some extremely rare use case :/
Dunno about the reason, it's mostly code we inherited and I just hacked around. And we probably never have witnessed your use case, so if anything, not adding footnote to footnotes pages is probably not doing something that doesn't make sense and not taking any risk with loops/reentrant stuff and re-displaying footnotes in footnotes possibly infinitely. Having said that, after having contemplated your only-30-lines changed for more than an hour, I believe it may be quite safe (may be by chance? taking the less intrusive path). lvrend.cpp: ok, even if we are in a end-of-book footnote line of text, we gather links in that line and associate them to the pagecontext line (we didn't before). Shouldn't hurt if we do nothing with them. lvpagesplitter.cpp:
So, if on a line we get footnote number 20 and 21 - and if 20 has nested footnotes 33 and 34, we will show at the bottom of pages: 20, 21, 33 and 34. I would expect to see 20, 33, 34 and only then 21, but I'm fine with the other (given how rare this is). (Dunno if inserting at j+1 instead of add()/appending would just solve that without any side effect. (B) (C) Somehow, it feels that adding them to (A) about crengine/crengine/include/lvpagesplitter.h Lines 250 to 253 in 60a5fbc
crengine/crengine/include/lvarray.h Lines 44 to 54 in 60a5fbc
I'm not super conforable with these types - and in the past I had some surprises (memory leaks, or crash while attempting to free an already freed thingie) while working on lvpagesplitter, so it may be why I stuck with long chain for accessing stuff like line->getLinks()->get(j) instead of using intermediate variables.In your case, I believe LVFootNoteList * notes{line->getLinks()}; will make a copy (on the stack) of that array. Am I right ? Or is it just a pointer to that same object?Often for nothing as in 99.999% of cases, we won't find any nested footnote (so small performance loss, which may be nothing in the grand scheme of what happens while splitting pages, dunno). But when we do, it will be added to that copy (instead of the main one). Dunno if destroy'ing that copy will destroy the item - and if later destroying the original could cause issues. And if destroying nested notes object could cause issues. Also dunno if we could just not need a copy and append it to the original one (it feels like they are not re-used). Also dunno if I'm just talking crap :) @benoit-pierre : can you give a third quick thought about this ? If it is safe and I'm just too worried ? If I got that right and we all think it is fine and we go on with that, I may suggest more and more accurate comments (the 2 comments you added feel a bit wrong), so it gets easier to get back into what all this is about. |
I think you got it all correct. Another reason why I think it's quite safe is that the book I've mainly used for testing actually has infinite recursion and this patch handles that. Note that all footnotes link back from their number to the origin location, so footnote 21 and 22 link back to footnote 20. If we didn't check correctly for duplicate entries, we'd get another instance of footnote 20 after 21 and 22.
Yes, that was intentional. But what's more critical and what I tried to explain in the comment: In an earlier version of this patch I checked I'll review the comments and try to make that clearer.
This just calls the copy constructor of
if you prefer. (1) is probably more the style of the current code anyway, I missed that when reviewing it myself before creating the PR. Edit Forgot about this:
I'll look into that, inserting at |
Does it ? Does it not call the snippet of lvarray.h I mentionned, actually creating a new array referencing all the items of the original one ? (Genuinely asking, I'm not really mastering C++ :/)
Dunno, is 1) then different from your original one, copying/referencing the same pointer to the object ? |
The |
these two just assign the pointer and to the best of my knowledge are identical:
These two are also identical, but invoke the copy constructor that you linked:
The reason I generally use Edit: Didn't refresh, so I missed @benoit-pierre's reply before sending mine 😅 |
63c0787
to
07af0ad
Compare
I need to do some more testing on this, will update later. |
Thanks for your answers. So, what would happen with |
crengine/src/lvpagesplitter.cpp
Outdated
for ( int nn=0; nn<nested_line->getLinks()->length(); nn++ ) { | ||
LVFootNote * nested_note = nested_line->getLinks()->get(nn); | ||
if ( notes->indexOf(nested_note) >= 0 ) | ||
continue; // Already referenced (recursively) on this page |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit not precise (as here, we don't look at the page, only at the current line list of notes which may or may not end up on this page. Suggestion (for my future rereading):
// Already referenced among the current lines notes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied
crengine/src/lvpagesplitter.cpp
Outdated
// Collect all nested footnotes before deciding if we delay the current note | ||
// e.g. Footnote 3 links to 4, which links to 3 | ||
// We need to see the whole chain to decide to not add footnote 3 twice | ||
// If we collected nested footnotes after `addFootnoteToPage(note_4)`, we might have | ||
// decided to delay footnote 4. Then, we're invoked again on the next page and think | ||
// we haven't seen footnote 3 yet (from the 4->3 link) and emit it again. | ||
// Collecting all nested footnotes (and skipping duplicates) gives us the same result | ||
// regardless of if some are delayed or not. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When reading this earlier, the problem and solution of delaying the current note was not what I thought was the problem we're handling here. And it has no impact on how we decide if we delay or not (we just look at the current note), so this had confused me quite a bit.
But I understand it was what you were solving and how you came to doing this that way, but from a blank state of mind, it is not obvious why this is written :)
I would write:
// Collect all nested footnotes and add them to the current line's list of footnotes links
// (avoiding duplicates) so we can just process them as if they were regular notes on that line.
// (^which is what I took some time to get to understand)
// Additionally, this helps .... (your stuff about the footnote chains, recursion, delayed... if you think it's worth
// mentionning - for me it's just implied, the same thing as with duplicates notes on the same line, so not
// really needed to understand what we're doing, but your choice :))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, will update the comment accordingly. Good idea to mention the big picture of what's going on, I lost that a bit.
I think the second part is still relevant because while it's implied that we avoid duplicates from the same line, that applies to the original line. The problem I'm trying to describe arises because we also go over all the lines in the (nested) footnotes. But we want to avoid duplicates in the context of the original line, and so we have to ensure we handle all of them here and check for duplicates. If we did (even part of) the recursion into nested notes after delaying footnotes to another page, we wouldn't know anymore which ones are duplicates in the original context.
It became clearer to me with how you phrased it, very helpful 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added your version of the first half.
I shrunk the second half. I hit the case I described earlier when testing extreme cases where I added more footnotes and nesting, but I think this was in a earlier version of this feature that did not get the detection for not showing nested footnotes inside the actual footnotes right and the problem I described here can't happen.
What could still happen though is if we hit delayed footnotes. Then we just flush out the notes that we accumulated via pushDelayedFootnotes
, which just calls addFootnoteToPage
and doesn't check for nested or duplicate notes again. It sill makes sense to mention here that we rely on doing these checks (per line) here.
I implemented this now and did some testing with additional levels of nesting and more footnotes on one page. It should work like you described now and does indeed make more sense. It couldn't be plain |
crengine/src/lvpagesplitter.cpp
Outdated
// | ||
// This needs to happen before we decide if the notes are added to this page or delayed | ||
// so delayed footnotes does not have to check for nested footnotes and duplicates again. | ||
int num_nested_notes = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To my French brain, "num" sounds more line "numero" (meaning the index of a note) than "number" (what you mean, "nombre" in French). I'd rather see it written as "nb" which avoids the possible confustion.
Fine with your comments (except the blank line :) May be wrap the previous ones shorter so you get a small third line and your next lines stand out better as a new paragraph (Yes, I dislike "Generic web browser paragraph style" for my books and for my code :))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done, thanks again for the review.
I actually never thought about it that way, but in my native German "num" would also read as "Nummer", i.e. index. Maybe the programming part of my brain is more compartmentalized to English, though that makes it hard to talk about code in any other language :)
@@ -8915,7 +8915,7 @@ void renderBlockElementEnhanced( FlowState * flow, ldomNode * enode, int x, int | |||
// See if there are links to footnotes in that line, and add | |||
// a reference to it so page splitting can bring the footnotes | |||
// text on this page, and then decide about page split. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add (so we know why the diff if we compare to the other similar handling like in legacy rendering):
// (We do this also if we are isFootNoteBody so we can handle footnotes nested in footnotes.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Let it sit for a day or two before merging, in case of nested afterthoughts :)
Thanks again. I'll be off for a few weeks to hopefully get some reading done 😁 Slightly off-topic here, but not sure where else to ask (gitter?) I have some more elaborate inline footnote improvements that I'm testing, but it's still pretty work-in-progress so I didn't open a PR yet. Specifically, I'm looking at porting the footnote extension logic for not perfectly formatted books from base/cre.cpp:_isLinkToFootnote down to crengine because in many of my books the inline footnotes are cut off currently. I got it working well in here, but it needs a lot of cleanup and I haven't found the correct place to put this logic yet (and need to adjust base afterwards to call this and keep the logic in one place). Would you prefer a draft PR early for this sort of change to discuss the design or immediate concerns? (will still be some weeks until I have time again) |
I'm fine without anything to review for weeks :) (I'm also pretty busy). I am really not fond of bringing this heuristics work from cre.cpp to crengine... |
Hi, just checking if there's anything I can do to push this over the line. For what it's worth I've not found any problems with it so far, and I've been doing a bunch more testing with more books and elaborate footnotes with this branch combined with #618. Thanks again for your thoughts and hints regarding the footnote extension topic by the way. |
It's fine and approved. |
hopefully more clear?
Instead of at the end. For example[1] like this[3] --- 1 Some note with nested[2] note 2 Nested note 3 Other note instead of For example[1] like this[3] --- 1 Some note with nested[2] note 3 Other note 2 Nested note
I hit the described case in testing degenerate cases, but I think this was in a earlier version of this feature that did not get the detection for not showing nested footnotes inside the actual footnotes right. What could still happen though is if we hit delayed footnotes. Then we just flush out the notes that we accumulated and don't check for nested ones or dulicates. It sill makes sense to mention here that we rely on doing these checks (per line) here.
For easier comparison to legacy handling
Motivation
I have a book (Wyrd Sisters by Terry Pratchett) that has nested footnotes like this (dropped text and formatted for clarity):
In
master
,pnXYZfn2
andpnXYZfn3
don't show up as inline footnotes.Change
Adds nested footnotes after their "parent" footnote instead of skipping them when inline footnotes are enabled.
Screenshots
Nested Footnote
Nested Footnote with Delayed Footnotes
Both footnote 21 and 22 are present, despite the lines referencing them being on different pages.
Still no footnotes in the non-inline footnotes
With inline footnotes enabled, footnotes in their original location (at the end of the book in this case) don't generate duplicate inline footnotes. This behavior is preserved. (I think this is the reason they were skipped completely before?)
Testing
I did some manual testing with the books that I own that have many footnotes of different formats.
This change is