-
Notifications
You must be signed in to change notification settings - Fork 5
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
Introduce support for styling documents with ANSI codes #25
Conversation
This commit introduces support for styling and rendering Docs with ANSI terminal control codes. The public interface uses smart constructors like "bold", "italic", and "underlined" to apply font styling to inner Docs. The implementation grows multiple new concepts to deal with this while still supporting the prerendering of Block elements. ANSIFont ======== The ANSIFont module introduces ADTs for various text properties that are supported by terminals, and for Fonts that indicate all the properties that should apply to a particular span of text. New fonts can be constructed by applying a StyleReq to an existing font, which replaces the requested property in the original font with the requested value. Attributed ========== The Attributed model introduces Attributed strings, which carry a Font along with an inner string type. It instantiates the HasChars class so that various features of the existing DocLayout code can somewhat seamlessly support rendering styled text. Building and rendering styled documents ======================================= Implementation outline: 1. Consumers add a smart constructor like `bold` to style a Doc. The inner doc gets wrapped in Doc's `Styled` constructor, indicating the text style requested for that block. 2. The renderer maintains a stack of `Font`s. When a `Styled` element is encountered, its `StyleReq` is applied to the `Font` on the top of the stack and pushed, the inner document is rendered, and then the font is popped and rendering continues. 3. The `Attributed a` returned by `prerender` can be rendered to `a` using `renderPlain`, which ignores all the font requests, or `renderANSI`, which adds the requisite control sequences to set the font every time the font changes. Conceptually, the renderer from `Doc a` to `Attributed a` turns the nested styling requests into a linear structure where every span of text carries the full set of font attributes it should be rendered with. The most interesting wrinkle to this implementation is that the contents of `Block` elements need to be prerendered by the `block` helper so they can be broken up into lines and filled, but we want to defer the decision of rendering plain text or ANSI-styled text until the final document is rendered. To support this, the `Block` constructor for a `Doc a` now carries an `Attributed a` in its lines field. Once the next rendering pass merges blocks together, instead of using `literal` to construct `Text` elements carring an `a` to render, it uses `cook` to construct `Cooked` elements with an `Attributed a` to be copied directly to the output stream _without looking at the font stack_. This means that the contents of a block are only ever styled by style requests that were made in the inner document of the block: the contents of `bold $ cblock n $ literal "x"` will be printed in plain text, whereas the contents of `cblock n $ bold $ literal "x"` will be bold.
Looks good to me! |
I'm running benchmarks:
vs before:
|
The "reflow" and "tabular" benchmarks show a very significant slowdown, which might be reason to revert or rethink this change. I'll see if I can quantify the impact on pandoc's benchmarks. Maybe you can see where the slowdown is coming from and do something to mitigate it? |
I’m not altogether surprised it’s slow, considering that I’m wrapping what would otherwise be a bunch of variously-optimized stringlike operations in a recursive ADT, and then doing a whole additional pass over it to render it to the final result. Sorry I didn’t think to quantify that before sending the PR. My design here is pretty much shaped by the fact that Blocks have to be prerendered to something that is It seems like less-naïve implementation of Maybe we can also win some performance back by replacing |
OK, I can now quantify the impact on pandoc: a few selected examples from the writer benchmarks. asciidoctor: from 3.2ms to 4.19ms |
Do you mean replace the definition of |
My branch |
looks like with the above-mentioned branch of |
That's really great! I'll wait for the PR. |
This PR introduces support for styling and rendering Docs with ANSI terminal control codes. The public interface uses smart constructors like “bold”, “italic”, and “underlined” to apply font styling to inner Docs. The implementation grows multiple new concepts to deal with this while still supporting the prerendering of Block elements.
NB: This is not quite complete; additional smart constructors are needed for the UI to support the full range of text styles (haven't done anything with color yet) and there's no tests of the actual ANSI output. Some additional work/thinking may be required to support the other two main features I want the pandoc writer to support: links (probably easy-ish, just needs to be supported by
Attributed a
) and images (maybe harder, but I think orthogonal to the features implemented here).ANSIFont
The ANSIFont module introduces ADTs for various text properties that are supported by terminals, and for Fonts that indicate all the properties that should apply to a particular span of text. New fonts can be constructed by applying a StyleReq to an existing font, which replaces the requested property in the original font with the requested value.
Attributed
The Attributed module introduces Attributed strings, which carry a Font along with an inner string type. It instantiates the HasChars class so that various features of the existing DocLayout code can somewhat seamlessly support rendering styled text.
Building and rendering styled documents
Implementation outline:
bold
to style a Doc. The inner doc gets wrapped in Doc’sStyled
constructor, indicating the text style requested for that block.Font
s. When aStyled
element is encountered, itsStyleReq
is applied to theFont
on the top of the stack and pushed, the inner document is rendered, and then the font is popped and rendering continues.Attributed a
returned byprerender
can be rendered toa
usingrenderPlain
, which ignores all the font requests, orrenderANSI
, which adds the requisite control sequences to set the font every time the font changes.Conceptually, the renderer from
Doc a
toAttributed a
turns the nested styling requests into a linear structure where every span of text carries the full set of font attributes it should be rendered with.The most interesting wrinkle to this implementation is that the contents of
Block
elements need to be prerendered by theblock
helper so they can be broken up into lines and filled, but we want to defer the decision of rendering plain text or ANSI-styled text until the final document is rendered. To support this, theBlock
constructor for aDoc a
now carries anAttributed a
in its lines field. Once the next rendering pass merges blocks together, instead of usingliteral
to constructText
elements carring ana
to render, it usescook
to constructCooked
elements with anAttributed a
to be copied directly to the output stream without looking at the font stack. This means that the contents of a block are only ever styled by style requests that were made in the inner document of the block: the contents ofbold $ cblock n $ literal "x"
will be printed in plain text, whereas the contents ofcblock n $ bold $ literal "x"
will be bold.