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

Styled documents interact poorly with line breaking. #30

Closed
silby opened this issue Mar 15, 2024 · 5 comments · Fixed by #31
Closed

Styled documents interact poorly with line breaking. #30

silby opened this issue Mar 15, 2024 · 5 comments · Fixed by #31

Comments

@silby
Copy link
Contributor

silby commented Mar 15, 2024

The inner document of a Styled can be a Concat, but as written, unfoldD won't unfold that document. The ultimate effect, via the definition of offsetOf, is that Styled text will exceed the line length when output because renderList (BreakingSpace : xs) can't correctly measure the offset of a Styled following a BreakingSpace.

It's not readily apparent what the right adaptation is here. Sprinkling cases around like unfoldD (Styled f x) = Styled f <$> unfoldD x and offsetOf (Styled _ x) = offsetOf x works towards addresses the line-breaking issue, but that then breaks how nested styles are flattened when outputting attributed text. That suggests we have to do some sort of further intermediate step but I'd have to think pretty hard about a good way of doing that.

@silby
Copy link
Contributor Author

silby commented Mar 15, 2024

Actually I think I do have a reasonable idea, involving some more Doc constructors for flattened out tags and these unfoldD cases:

unfoldD (Linked l x) = LinkOpen l : unfoldD x <> [LinkClose]
unfoldD (Styled f x) = StyleOpen f : unfoldD x <> [StyleClose]

seems to work but probably no PR til next week

@silby
Copy link
Contributor Author

silby commented Mar 19, 2024

I am approaching a PR that is sort of like what I proposed a few days ago but instead of adding cases to unfoldD, instead introduces a disjoint FlatDoc a type to serve as a kind of intermediate representation for Doc a values, with Concats unfolded to lists, with the taglike transformation of Styled to FStyleOpen and FStyleClose, Emptys eliminated, and everything else translated roughly 1:1 from Bar to FBar. (Among other things this eliminates the runtime-error case from renderList.)

In working on this I realized that the busted line-breaking was a condition that already existed in DocLayout for the Doc types that have inner documents, like Prefixed, BeforeNonBlank, and Flush. For instance:

ghci> let p = "hi" <+> (prefixed "x" "mom")
ghci> render (Just 2) p
"hi\nxmom"
ghci> render (Just 3) p
"hi mom"

This boils down to, like with Styled and Linked, the default offsetOf _ = 0 case when deciding whether to break a line; the Prefixed doc doesn't get peeked into. unfoldD flattens all the Concats from the top-level doc, but any inner docs are not flattened and can't have their offsets and contents checked in the same way.

Is there any particular design intention for, say, how a Prefixed doc having a preceding doc on the same line should work? It's reasonable that nobody expected anyone to do this on purpose. Styled docs have basically the same structure as Prefixed but have to behave more reliably to produce good terminal output, hence my current work in progress on flattening out Doc more thoroughly. I would say that it'd "more correct" for p in my example to be rendered as "hi\nxmom" at both width 2 and width 3, since "hi mom" is after all wider than 3 terminal cells. But I'm probably not going to follow this thread further right now because it seems unlikely to matter to anyone or anything in real life and it would probably entail even more fiddling with renderList cases than I already have going.

@jgm
Copy link
Owner

jgm commented Mar 19, 2024

Is there any particular design intention for, say, how a Prefixed doc having a preceding doc on the same line should work? It's reasonable that nobody expected anyone to do this on purpose.

No intention. I just didn't anticipate that anyone would do this.

Incidentally, it occurred to me that it could be worth experimenting with an alternative way of approaching Doc that makes it a Seq of Doc' elements. This might avoid the need to have both Doc and FlatDoc, and it would avoid the need for a 'unfolding' or 'flattening' operation. I haven't really looked into this, so there might be an obvious reason why it would be a bad idea, but it's worth a thought.

@silby
Copy link
Contributor Author

silby commented Mar 22, 2024

I like that idea and I will experiment with that.

@silby
Copy link
Contributor Author

silby commented Mar 22, 2024

I've spent an hour or so on the Seq idea and it doesn't feel rapidly tractable. I've run into head-scratchers reimplementing things like getOffset and chomp. I still think it's a plausible idea to have a somewhat-flat or completely-flat structure of constructors by default Concat sometime, but I think it's too much work for me to tackle right now when I really want to get back to the ANSI writer itself. I'll finish up with my original idea and see what you think of it.

If you wanted to prepare for changing what's inside of a Doc in the future you could do an API-breaking release that just stops exporting the Doc constructors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants