%% ===================================================================== %% This library is free software; you can redistribute it and/or modify %% it under the terms of the GNU Lesser General Public License as %% published by the Free Software Foundation; either version 2 of the %% License, or (at your option) any later version. %% %% This library is distributed in the hope that it will be useful, but %% WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %% Lesser General Public License for more details. %% %% You should have received a copy of the GNU Lesser General Public %% License along with this library; if not, write to the Free Software %% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 %% USA %% %% @copyright 2000-2006 Richard Carlsson %% @author Richard Carlsson %% @end %% ===================================================================== %% @doc A generic pretty printer library. This module uses a %% strict-style context passing implementation of John Hughes algorithm, %% described in "The design of a Pretty-printing Library". The %% paragraph-style formatting, empty documents, floating documents, and %% null strings are my own additions to the algorithm. %% %% To get started, you should read about the {@link document()} data %% type; the main constructor functions: {@link text/1}, {@link %% above/2}, {@link beside/2}, {@link nest/2}, {@link sep/1}, and {@link %% par/2}; and the main layout function {@link format/3}. %% %% If you simply want to format a paragraph of plain text, you probably %% want to use the {@link text_par/2} function, as in the following %% example: %% ``` %% prettypr:format(prettypr:text_par("Lorem ipsum dolor sit amet"), 20) %% ''' %% @TODO can floats be moved in/out of sep:s without too much pain? -module(prettypr). -export([above/2, beside/2, best/3, break/1, empty/0, floating/1, floating/3, follow/2, follow/3, format/1, format/2, format/3, nest/2, par/1, par/2, sep/1, text/1, null_text/1, text_par/1, text_par/2]). -export_type([document/0]). %% --------------------------------------------------------------------- -type deep_string() :: [char() | deep_string()]. %% Document structures fully implemented and available to the user: -record(text, {s :: deep_string()}). -record(nest, {n :: integer(), d :: document()}). -record(beside, {d1 :: document(), d2 :: document()}). -record(above, {d1 :: document(), d2 :: document()}). -record(sep, {ds :: [document()], i = 0 :: integer(), p = false :: boolean()}). %% Document structure which is not clear whether it is fully implemented: -record(float, {d :: document(), h :: integer(), v :: integer()}). %% Document structures not available to the user: -record(union, {d1 :: document(), d2 :: document()}). -record(fit, {d :: document()}). %% --------------------------------------------------------------------- %% A small warning for hackers: it's fairly easy to break this %% thing (in particular, to muck up the complexity) if you don't %% understand how it works. %% --------------------------------------------------------------------- %% ===================================================================== %% @type document(). An abstract character-based "document" representing %% a number of possible layouts, which can be processed to produce a %% single concrete layout. A concrete layout can then be rendered as a %% sequence of characters containing linebreaks, which can be passed to %% a printer or terminal that uses a fixed-width font. %% %% For example, a document `sep([text("foo"), text("bar")])' %% represents the two layouts %% ```foo bar''' %% and %% ```foo %% bar''' %% %% Which layout is chosen depends on the available horizontal space. %% When processing a document, the main parameters are the paper %% width and the line width (also known as the "ribbon %% width"). In the resulting layout, no text should be printed beyond %% the paper width (which by default is 80 characters) as long as it can %% be avoided, and each single line of text (its indentation not %% counted, hence "ribbon") should preferably be no wider than the %% specified line width (which by default is 65). %% %% Documents can be joined into a single new document using the %% constructor functions of this module. Note that the new document %% often represents a larger number of possible layouts than just the %% sum of the components. -type document() :: 'null' | #text{} | #nest{} | #beside{} | #above{} | #sep{} | #float{} | #union{} | #fit{}. %% ===================================================================== %% @spec text(Characters::string()) -> document() %% %% @doc Yields a document representing a fixed, unbreakable sequence of %% characters. The string should contain only printable %% characters (tabs allowed but not recommended), and not %% newline, line feed, vertical tab, etc. A tab character (`\t') is %% interpreted as padding of 1-8 space characters to the next column of %% 8 characters within the string. %% %% @see empty/0 %% @see null_text/1 %% @see text_par/2 -spec text(string()) -> #text{}. text(S) -> mktext(string(S)). % convert to internal representation %% This function is used internally only, and expects a string on %% the internal representation: mktext(S) -> #text{s = S}. %% ===================================================================== %% @spec null_text(Characters::string()) -> document() %% %% @doc Similar to {@link text/1}, but the result is treated as having %% zero width. This is regardless of the actual length of the string. %% Null text is typically used for markup, which is supposed to have no %% effect on the actual layout. %% %% The standard example is when formatting source code as HTML to be %% placed within `
...
' markup, and using e.g. `' and `' %% to make parts of the source code stand out. In this case, the markup %% does not add to the width of the text when viewed in an HTML browser, %% so the layout engine should simply pretend that the markup has zero %% width. %% %% @see text/1 %% @see empty/0 -spec null_text(string()) -> #text{}. null_text(S) -> mktext(null_string(S)). % convert to internal representation %% ===================================================================== %% @spec text_par(Text::string()) -> document() %% @equiv text_par(Text, 0) -spec text_par(string()) -> document(). text_par(S) -> text_par(S, 0). %% ===================================================================== %% @spec text_par(Text::string(), Indentation::integer()) -> document() %% %% @doc Yields a document representing paragraph-formatted plain text. %% The optional `Indentation' parameter specifies the extra indentation %% of the first line of the paragraph. For example, `text_par("Lorem %% ipsum dolor sit amet", N)' could represent %% ```Lorem ipsum dolor %% sit amet''' %% if `N' = 0, or %% ``` Lorem ipsum %% dolor sit amet''' %% if `N' = 2, or %% ```Lorem ipsum dolor %% sit amet''' %% if `N' = -2. %% %% (The sign of the indentation is thus reversed compared to the {@link %% par/2} function, and the behaviour varies slightly depending on the %% sign in order to match the expected layout of a paragraph of text.) %% %% Note that this is just a utility function, which does all the work of %% splitting the given string into words separated by whitespace and %% setting up a {@link par/2. `par'} with the proper indentation, %% containing a list of {@link text/1. `text'} elements. %% %% @see text_par/1 %% @see text/1 %% @see par/2 -spec text_par(string(), integer()) -> document(). text_par(S, 0) -> par(words(S)); text_par(S, N) when N > 0 -> nest(N, par(words(S), -N)); text_par(S, N) when N < 0 -> par(words(S), -N). words(S) -> words(S, [], []). words([$\s | Cs], As, Ws) -> words_1(Cs, As, Ws); words([$\t | Cs], As, Ws) -> words_1(Cs, As, Ws); words([$\n | Cs], As, Ws) -> words_1(Cs, As, Ws); words([C | Cs], As, Ws) -> words(Cs, [C | As], Ws); words([], [], Ws) -> lists:reverse(Ws); words([], As, Ws) -> words_1([], As, Ws). words_1(Cs, [], Ws) -> words(Cs, [], Ws); words_1(Cs, As, Ws) -> words(Cs, [], [text(lists:reverse(As)) | Ws]). %% ===================================================================== %% @spec empty() -> document() %% %% @doc Yields the empty document, which has neither height nor width. %% (`empty' is thus different from an empty {@link text/1. `text'} %% string, which has zero width but height 1.) %% %% Empty documents are occasionally useful; in particular, they have the %% property that `above(X, empty())' will force a new line after `X' %% without leaving an empty line below it; since this is a common idiom, %% the utility function {@link break/1} will place a given document in %% such a context. %% %% @see text/1 -spec empty() -> 'null'. empty() -> null. %% ===================================================================== %% @spec break(document()) -> document() %% %% @doc Forces a line break at the end of the given document. This is a %% utility function; see {@link empty/0} for details. -spec break(document()) -> #above{}. break(D) -> above(D, empty()). %% ===================================================================== %% @spec nest(N::integer(), D::document()) -> document() %% %% @doc Indents a document a number of character positions to the right. %% Note that `N' may be negative, shifting the text to the left, or %% zero, in which case `D' is returned unchanged. -spec nest(integer(), document()) -> document(). nest(N, D) -> if N =:= 0 -> D; true -> #nest{n = N, d = D} end. %% ===================================================================== %% @spec beside(D1::document(), D2::document()) -> document() %% %% @doc Concatenates documents horizontally. Returns a document %% representing the concatenation of the documents `D1' and `D2' such %% that the last character of `D1' is horizontally adjacent to the first %% character of `D2', in all possible layouts. (Note: any indentation of %% `D2' is lost.) %% %% Examples: %% ```ab cd => abcd %% %% ab ef ab %% cd gh => cdef %% gh''' -spec beside(document(), document()) -> #beside{}. beside(D1, D2) -> #beside{d1 = D1, d2 = D2}. %% ===================================================================== %% @spec above(D1::document(), D2::document()) -> document() %% %% @doc Concatenates documents vertically. Returns a document %% representing the concatenation of the documents `D1' and `D2' such %% that the first line of `D2' follows directly below the last line of %% `D1', and the first character of `D2' is in the same horizontal %% column as the first character of `D1', in all possible layouts. %% %% Examples: %% ```ab cd => ab %% cd %% %% abc %% abc fgh => de %% de ij fgh %% ij''' -spec above(document(), document()) -> #above{}. above(D1, D2) -> #above{d1 = D1, d2 = D2}. %% ===================================================================== %% @spec sep(Docs::[document()]) -> document() %% %% @doc Arranges documents horizontally or vertically, separated by %% whitespace. Returns a document representing two alternative layouts %% of the (nonempty) sequence `Docs' of documents, such that either all %% elements in `Docs' are concatenated horizontally, and separated by a %% space character, or all elements are concatenated vertically (without %% extra separation). %% %% Note: If some document in `Docs' contains a line break, the vertical %% layout will always be selected. %% %% Examples: %% ``` ab %% ab cd ef => ab cd ef | cd %% ef %% %% ab ab %% cd ef => cd %% ef''' %% %% @see par/2 -spec sep([document()]) -> #sep{}. sep(Ds) -> #sep{ds = Ds}. %% ===================================================================== %% @spec par(Docs::[document()]) -> document() %% @equiv par(Ds, 0) -spec par([document()]) -> #sep{}. par(Ds) -> par(Ds, 0). %% ===================================================================== %% @spec par(Docs::[document()], Offset::integer()) -> document() %% %% @doc Arranges documents in a paragraph-like layout. Returns a %% document representing all possible left-aligned paragraph-like %% layouts of the (nonempty) sequence `Docs' of documents. Elements in %% `Docs' are separated horizontally by a single space character and %% vertically with a single line break. All lines following the first %% (if any) are indented to the same left column, whose indentation is %% specified by the optional `Offset' parameter relative to the position %% of the first element in `Docs'. For example, with an offset of -4, %% the following layout can be produced, for a list of documents %% representing the numbers 0 to 15: %% %% ``` 0 1 2 3 %% 4 5 6 7 8 9 %% 10 11 12 13 %% 14 15''' %% or with an offset of +2: %% ```0 1 2 3 4 5 6 %% 7 8 9 10 11 %% 12 13 14 15''' %% %% The utility function {@link text_par/2} can be used to easily %% transform a string of text into a `par' representation by splitting %% it into words. %% %% Note that whenever a document in `Docs' contains a line break, it %% will be placed on a separate line. Thus, neither a layout such as %% ```ab cd %% ef''' %% nor %% ```ab %% cd ef''' %% will be generated. However, a useful idiom for making the former %% variant possible (when wanted) is `beside(par([D1, text("")], N), %% D2)' for two documents `D1' and `D2'. This will break the line %% between `D1' and `D2' if `D1' contains a line break (or if otherwise %% necessary), and optionally further indent `D2' by `N' character %% positions. The utility function {@link follow/3} creates this context %% for two documents `D1' and `D2', and an optional integer `N'. %% %% @see par/1 %% @see text_par/2 -spec par([document()], integer()) -> #sep{}. par(Ds, N) -> mksep(Ds, N, true). %% Used internally only: mksep(Ds, N, P) when is_integer(N) -> #sep{ds = Ds, i = N, p = P}. %% ===================================================================== %% @spec follow(D1::document(), D2::document()) -> document() %% @equiv follow(D1, D2, 0) -spec follow(document(), document()) -> #beside{}. follow(D1, D2) -> follow(D1, D2, 0). %% ===================================================================== %% @spec follow(D1::document(), D2::document(), Offset::integer()) -> %% document() %% %% @doc Separates two documents by either a single space, or a line %% break and intentation. In other words, one of the layouts %% ```abc def''' %% or %% ```abc %% def''' %% will be generated, using the optional offset in the latter case. This %% is often useful for typesetting programming language constructs. %% %% This is a utility function; see {@link par/2} for further details. %% %% @see follow/2 -spec follow(document(), document(), integer()) -> #beside{}. follow(D1, D2, N) when is_integer(N) -> beside(par([D1, nil()], N), D2). %% ===================================================================== %% @spec floating(document()) -> document() %% @equiv floating(D, 0, 0) -spec floating(document()) -> #float{}. floating(D) -> floating(D, 0, 0). %% ===================================================================== %% @spec floating(D::document(), Hp::integer(), Vp::integer()) -> %% document() %% %% @doc Creates a "floating" document. The result represents the same %% set of layouts as `D'; however, a floating document may be moved %% relative to other floating documents immediately beside or above it, %% according to their relative horizontal and vertical priorities. These %% priorities are set with the `Hp' and `Vp' parameters; if omitted, %% both default to zero. %% %% Notes: Floating documents appear to work well, but are currently less %% general than you might wish, losing effect when embedded in certain %% contexts. It is possible to nest floating-operators (even with %% different priorities), but the effects may be difficult to predict. %% In any case, note that the way the algorithm reorders floating %% documents amounts to a "bubblesort", so don't expect it to be able to %% sort large sequences of floating documents quickly. -spec floating(document(), integer(), integer()) -> #float{}. floating(D, H, V) when is_integer(H), is_integer(V) -> #float{d = D, h = H, v = V}. %% ===================================================================== %% @spec format(D::document()) -> string() %% @equiv format(D, 80) -spec format(document()) -> string(). format(D) -> format(D, 80). %% ===================================================================== %% @spec format(D::document(), PaperWidth::integer()) -> string() %% @equiv format(D, PaperWidth, 65) -spec format(document(), integer()) -> string(). format(D, W) -> format(D, W, 65). %% ===================================================================== %% @spec format(D:: document(), PaperWidth::integer(), %% LineWidth::integer()) -> string() %% @throws no_layout %% %% @doc Computes a layout for a document and returns the corresponding %% text. See {@link document()} for further information. Throws %% `no_layout' if no layout could be selected. %% %% `PaperWidth' specifies the total width (in character positions) of %% the field for which the text is to be laid out. `LineWidth' specifies %% the desired maximum width (in number of characters) of the text %% printed on any single line, disregarding leading and trailing white %% space. These parameters need to be properly balanced in order to %% produce good layouts. By default, `PaperWidth' is 80 and `LineWidth' %% is 65. %% %% @see best/3 -spec format(document(), integer(), integer()) -> string(). format(D, W, R) -> case best(D, W, R) of empty -> throw(no_layout); L -> layout(L) end. %% ===================================================================== %% Representation: %% %% document() = #text{s = string()} %% | #nest{n = integer(), d = document()} %% | #beside{d1 = document(), d2 = document()} %% | #above{d1 = document(), d2 = document()} %% | #sep{ds = [document()], i = integer(), p = boolean()} %% | null %% %% A `text' node simply represents a string (which should not contain %% linefeed or carriage return characters). A `nest' node specifies a %% relative indentation (in number of character positions) of a %% document. The indentation could be a negative number. A `beside' node %% specifies a horizontal composition of two documents, and an `above' %% node a vertical composition. A `sep' node specifies a list of %% alternative documents; the `i' field holds the extra indentation of %% all documents but the first in `ds', and if the `p' field is `true' %% then the list is typeset in paragraph mode. %% %% The function `best/3' yields a representation of a "best layout", %% suitable for direct conversion to text, having the following %% restricted form: %% %% layout() = #text{s = string()} %% | #above{d1 = #text{s = string()}, d2 = layout()} %% | #nest{n = integer(), d = layout()} %% | null %% %% The function `layout/1' performs the final transformation to a single %% flat string from the restricted document form. layout(L) -> lists:reverse(layout(0, L, [])). layout(N, #above{d1 = #text{s = S}, d2 = L}, Cs) -> layout(N, L, [$\n | flatrev(string_chars(S), indent(N, Cs))]); layout(N, #nest{n = N1, d = L}, Cs) -> layout(N + N1, L, Cs); layout(N, #text{s = S}, Cs) -> flatrev(string_chars(S), indent(N, Cs)); layout(_N, null, Cs) -> Cs. indent(N, Cs) when N >= 8 -> indent(N - 8, [$\t | Cs]); indent(N, Cs) when N > 0 -> indent(N - 1, [$\s | Cs]); indent(_N, Cs) -> Cs. flatrev(Cs, As) -> flatrev(Cs, As, []). flatrev([C = [_|_] | Cs], As, Ss) -> flatrev(C, As, [Cs | Ss]); flatrev([[] | Cs], As, Ss) -> flatrev(Cs, As, Ss); flatrev([C | Cs], As, Ss) -> flatrev(Cs, [C | As], Ss); flatrev([], As, [S | Ss]) -> flatrev(S, As, Ss); flatrev([], As, []) -> As. %% ===================================================================== %% @spec best(document(), PaperWidth::integer(), %% LineWidth::integer()) -> empty | document() %% %% @doc Selects a "best" layout for a document, creating a corresponding %% fixed-layout document. If no layout could be produced, the atom %% `empty' is returned instead. For details about `PaperWidth' and %% `LineWidth', see {@link format/3}. The function is idempotent. %% %% One possible use of this function is to compute a fixed layout for a %% document, which can then be included as part of a larger document. %% For example: %% ```above(text("Example:"), nest(8, best(D, W - 12, L - 6)))''' %% will format `D' as a displayed-text example indented by 8, whose %% right margin is indented by 4 relative to the paper width `W' of the %% surrounding document, and whose maximum individual line length is %% shorter by 6 than the line length `L' of the surrounding document. %% % This function is used by the {@link format/3} function to prepare a %% document before being laid out as text. %% Recall that a document represents a set of possible layouts. `best' %% selects the "best" layout of a document, returning a simplified %% representation that can be given directly to `layout', unless the %% returned value is `empty', signaling that no layout could be %% produced. In addition, documents on the form `#union{d1 = D1, d2 = %% D2}' and `#fit{d = D}' are used internally. %% %% Note: It is vital for this algorithm to maintain the invariant on %% unions that the left argument has a longer first line than the right %% argument! %% Contexts: %% %% #c_best_nest{w = integer(), r = integer(), i = integer()} %% #c_above_nest{d = document(), i = integer(), c = ctxt()} %% #c_beside{d = document(), c = ctxt()} %% #c_text_beside{s = string(), c = ctxt()} %% #c_sep_nest{ds = [document()], i = integer(), p = boolean(), %% c = ctxt()} %% #c_best_nest_or{w = integer(), r = integer(), i = integer(), %% d = document()} %% #c_fit{c = ctxt()} %% best(w, r, nest(i, *)) -record(c_best_nest, {w :: integer(), r :: integer(), i :: integer()}). %% above(*, nest(i, d)) -record(c_above_nest, {d :: document(), i = 0 :: integer(), c :: ctxt()}). -record(c_beside, {d :: document(), c :: ctxt()}). %% beside(*, d) -record(c_text_beside, {s :: string(), c :: ctxt()}). %% beside(text(s), *) %% p = false => sep([* | map(nest i, ds)]) %% p = true => par([* | map(nest i, ds)]) -record(c_sep_nest, {ds :: [document()], i :: integer(), p :: boolean(), c :: ctxt()}). %% nicest(best(w, r, nest(i, *)), best(w, r, d)) -record(c_best_nest_or, {w :: integer(), r :: integer(), i :: integer(), d :: document()}). -record(c_fit, {c :: ctxt()}). %% fit(*) %% beside(float(d, h, v), *) -record(c_float_beside, {d :: document(), h :: integer(), v :: integer(), c :: ctxt()}). %% above(float(d, h, v), nest(i, *)) -record(c_float_above_nest, {d :: document(), h :: integer(), v :: integer(), i :: integer(), c :: ctxt()}). %% Contexts introduced: In case: %% %% c_best_nest top-level call %% c_above_nest above (c_best_nest) %% c_beside beside (c_best_nest) %% c_text_beside text (c_beside) %% c_sep_nest sep (c_best_nest) %% c_best_nest_or union (c_best_nest) %% c_fit fit %% c_float_beside float (c_beside) %% c_float_above_nest float (c_above_nest) -type ctxt() :: #c_best_nest{} | #c_above_nest{} | #c_beside{} | #c_text_beside{} | #c_sep_nest{} | #c_best_nest_or{} | #c_fit{} | #c_float_beside{} | #c_float_above_nest{}. %% Entry point for the layout algorithm: -spec best(document(), integer(), integer()) -> 'empty' | document(). best(D, W, R) -> rewrite(D, #c_best_nest{w = W, r = R, i = 0}). rewrite(#text{s = S}, C) -> case C of #c_best_nest{i = N} -> nest(N, mktext(S)); % finish #c_above_nest{d = D1, i = N1, c = C1} -> case C1 of #c_best_nest{w = W, r = R, i = N} -> %% Move out completed line. %% (Note new indentation N1.) nest(N, above(mktext(S), rewrite(D1, #c_best_nest{w = W - N, r = R, i = N1}))); #c_beside{d = D2, c = C2} -> %% Associativity (not symmetric) rewrite(above(mktext(S), nest(N1, beside(D1, D2))), C2); #c_text_beside{s = S1, c = C2} -> %% Join segments (note the indentation!) rewrite(above(mktext(concat(S1, S)), nest(N1 + width(S1), D1)), C2); #c_sep_nest{ds = Ds, i = N, c = C2} -> case is_empty_string(S) of false -> %% Move out the prefix (note the %% indentation!) W = width(S), rewrite(beside( mktext(S), mksep([above(nil(), nest(N1 - W, D1)) | Ds], N - W, C1#c_sep_nest.p)), C2); true -> %% Like when we have just an empty %% string and nothing else, this %% forces us to expand the `sep'. The %% line break will then force a normal %% `sep' to select the vertical %% alternative, but for a `par', we %% need to force a line break before %% the remaining elements are laid %% out. (Note the indentation!) case C1#c_sep_nest.p of false -> rewrite(expand_sep( above(nil(), nest(N1, D1)), Ds, N), C2); true -> rewrite(expand_par( above(nil(), nest(N1, D1)), Ds, N), C2) end end; #c_best_nest_or{w = W, r = R, i = N, d = D} -> L = width(S), case ((L + N) > W) or (L > R) of true -> %% The first line of the LHS layout is %% not nice, so select the RHS. rewrite(D, #c_best_nest{w = W, r = R, i = N}); false -> %% Select the LHS. (Note the %% indentation!) rewrite(above(mktext(S), nest(N1, D1)), #c_best_nest{w = W, r = R, i = N}) end; #c_float_beside{d = D2, c = C2} -> rewrite(beside(D2, above(mktext(S), nest(N1, D1))), C2); #c_float_above_nest{d = D2, i = N2, c = C2} -> rewrite(above(D2, nest(N2, above(mktext(S), nest(N1, D1)))), C2); #c_above_nest{} -> exit(badarg); % this can't happen #c_fit{} -> exit(badarg) % this can't happen end; #c_beside{d = D1, c = C1} -> case C1 of #c_above_nest{d = D2, i = N, c = C2} -> case is_empty_string(S) of false -> %% Move out the prefix (note the %% indentation!) W = width(S), rewrite(beside(mktext(S), above( beside(nil(), D1), nest(N - W, D2))), C2); true -> %% Pass on rewrite(D1, #c_text_beside{s = S, c = C1}) end; #c_text_beside{s = S1, c = C2} -> %% Associativity (we simplify early) rewrite(beside(mktext(concat(S1, S)), D1), C2); #c_sep_nest{ds = Ds, i = N, c = C2} -> case is_empty_string(S) of false -> %% Move out the prefix (note the %% indentation!) W = width(S), rewrite(beside(mktext(S), mksep( [beside(nil(), D1) | Ds], N - W, C1#c_sep_nest.p)), C2); true -> %% Pass on rewrite(D1, #c_text_beside{s = S, c = C1}) end; #c_best_nest_or{w = W, r = R, i = N, d = D} -> L = width(S), case ((L + N) > W) or (L > R) of true -> %% The first line of the LHS layout is %% not nice, so select the RHS. rewrite(D, #c_best_nest{w = W, r = R, i = N}); false -> %% Pass on rewrite(D1, #c_text_beside{s = S, c = C1}) end; #c_float_beside{d = D2, c = C2} -> rewrite(beside(D2, beside(mktext(S), D1)), C2); #c_float_above_nest{d = D2, i = N, c = C2} -> rewrite(above(D2, nest(N, beside(mktext(S), D1))), C2); _ -> %% Pass on rewrite(D1, #c_text_beside{s = S, c = C1}) end; #c_text_beside{s = S1, c = C1} -> rewrite(mktext(concat(S1, S)), C1); % join segments #c_sep_nest{ds = Ds, i = N, c = C1} -> case is_empty_string(S) of false -> %% Move out the prefix (note the indentation!) rewrite(beside(mktext(S), mksep([nil() | Ds], N - width(S), C#c_sep_nest.p)), C1); true -> %% This is the only place where we are forced to %% introduce a union. Recall the invariant that the %% left argument must have a longer first line than %% the right argument; also recall that `Ds' is %% always nonempty here. Now, since [D | Ds] %% contains at least two elements, the first line of %% the horizontal layout will always contain at %% least one space character more than the first %% line of the vertical layout. case C#c_sep_nest.p of false -> rewrite(expand_sep(nil(), Ds, N), C1); true -> rewrite(expand_par(nil(), Ds, N), C1) end end; #c_best_nest_or{w = W, r = R, i = N, d = D} -> L = width(S), case ((L + N) > W) or (L > R) of true -> %% The first line of the LHS layout is not %% nice, so select the RHS (which contains %% at least two lines). rewrite(D, #c_best_nest{w = W, r = R, i = N}); false -> nest(N, mktext(S)) % finish end; #c_fit{c = C1} -> %% Identity: rewrite(mktext(S), C1); #c_float_beside{d = D1, c = C1} -> rewrite(beside(D1, mktext(S)), C1); #c_float_above_nest{d = D1, i = N, c = C1} -> rewrite(above(D1, nest(N, mktext(S))), C1) end; rewrite(#nest{n = N, d = D}, C) -> case C of #c_best_nest{w = W, r = R, i = N1} -> %% Note that we simplify by not creating an actual `nest' %% node, but instead just modifying the context: %% rewrite(nest(N1, nest(N, D))) = rewrite(nest(N1 + N, D)). rewrite(D, #c_best_nest{w = W, r = R, i = N + N1}); #c_above_nest{d = D1, i = N1, c = C1} -> %% Distributivity %% (Note the indentation!) rewrite(nest(N, above(D, nest(N1 - N, D1))), C1); #c_beside{d = D1, c = C1} -> %% Associativity (not symmetric): rewrite(nest(N, beside(D, D1)), C1); #c_text_beside{} -> rewrite(D, C); % (`beside' kills RHS indentation) #c_sep_nest{ds = Ds, i = N1, c = C1} -> %% Distributivity (in the vertical form, the RHS %% indentation is killed) rewrite(nest(N, mksep([D | Ds], N1 - N, C#c_sep_nest.p)), C1); #c_fit{c = C1} -> %% Distributivity: rewrite(nest(N, fit(D)), C1); #c_float_beside{} -> rewrite(D, C); % (`beside' kills RHS indentation) #c_float_above_nest{d = D1, h = H, v = V, i = N1, c = C1} -> rewrite(D, #c_float_above_nest{d = D1, h = H, v = V, i = N + N1, c = C1}); #c_best_nest_or{} -> exit(badarg) % this can't happen end; rewrite(#above{d1 = D1, d2 = D2}, C) -> case C of #c_above_nest{d = D3, i = N, c = C1} -> %% Associativity: %% (Note the indentation!) rewrite(D1, #c_above_nest{d = above(D2, nest(N, D3)), c = C1}); #c_beside{d = D3, c = C1} -> %% Associativity (not symmetric): rewrite(above(D1, beside(D2, D3)), C1); #c_fit{c = C1} -> rewrite(empty, C1); % this is the whole point of `fit' _ -> rewrite(D1, #c_above_nest{d = D2, c = C}) % pass on end; rewrite(#beside{d1 = D1, d2 = D2}, C) -> case C of #c_beside{d = D3, c = C1} -> %% Associativity: rewrite(D1, #c_beside{d = beside(D2, D3), c = C1}); #c_fit{c = C1} -> %% Distributivity: rewrite(beside(fit(D1), fit(D2)), C1); _ -> rewrite(D1, #c_beside{d = D2, c = C}) % pass on end; rewrite(#sep{ds = Ds, i = N, p = P}, C) -> case C of #c_fit{c = C1} -> %% The vertical layout is thus impossible, and the %% extra indentation has no effect. rewrite(fit(horizontal(Ds)), C1); #c_float_beside{d = D1, c = C1} -> %% Floats are not moved in or out of sep's rewrite(beside(D1, mksep(Ds, N, P)), C1); #c_float_above_nest{d = D1, i = N1, c = C1} -> %% Floats are not moved in or out of sep's rewrite(above(D1, nest(N1, mksep(Ds, N, P))), C1); _ -> enter_sep(Ds, N, P, C) % pass on end; rewrite(#union{d1 = D1, d2 = D2}, C) -> %% Introduced by the occurrence of an empty `text' string in a %% `sep' context. See the note above about the invariant for %% unions! case C of #c_best_nest{w = W, r = R, i = N} -> %% Pass on rewrite(D1, #c_best_nest_or{w = W, r = R, i = N, d = D2}); #c_above_nest{d = D3, i = N, c = C1} -> %% Distributivity: %% (Note the indentation!) rewrite(union(above(D1, nest(N, D3)), above(D2, nest(N, D3))), C1); #c_beside{d = D3, c = C1} -> %% Distributivity: rewrite(union(beside(D1, D3), beside(D2, D3)), C1); #c_text_beside{s = S, c = C1} -> %% Distributivity: rewrite(union(beside(mktext(S), D1), beside(mktext(S), D2)), C1); #c_sep_nest{ds = Ds, i = N, c = C1} -> %% Distributivity: rewrite(union(mksep([D1 | Ds], N, C#c_sep_nest.p), mksep([D2 | Ds], N, C#c_sep_nest.p)), C1); #c_best_nest_or{w = W, r = R, i = N, d = D3} -> %% Associativity: rewrite(D1, #c_best_nest_or{w = W, r = R, i = N, d = union(D2, D3)}); #c_fit{c = C1} -> %% Distributivity: rewrite(union(fit(D1), fit(D2)), C1); #c_float_beside{d = D3, h = H, v = V, c = C1} -> %% Distributivity: rewrite(union(beside(floating(D3, H, V), D1), beside(floating(D3, H, V), D2)), C1); #c_float_above_nest{d = D3, h = H, v = V, i = N, c = C1} -> %% Distributivity: rewrite(union(above(floating(D3, H, V), nest(N, D1)), above(floating(D3, H, V), nest(N, D2))), C1) end; rewrite(empty, C) -> %% Introduced by `sep'. case C of #c_best_nest{} -> empty; % preserve `empty' #c_above_nest{c = C1} -> rewrite(empty, C1); % preserve `empty' #c_beside{c = C1} -> rewrite(empty, C1); % preserve `empty' #c_text_beside{c = C1} -> rewrite(empty, C1); % preserve `empty' #c_sep_nest{c = C1} -> rewrite(empty, C1); % preserve `empty' #c_best_nest_or{w = W, r = R, i = N, d = D} -> %% Try the other layout rewrite(D, #c_best_nest{w = W, r = R, i = N}); #c_fit{c = C1} -> rewrite(empty, C1); % preserve `empty' #c_float_beside{c = C1} -> rewrite(empty, C1); % preserve `empty' #c_float_above_nest{c = C1} -> rewrite(empty, C1) % preserve `empty' end; rewrite(#fit{d = D}, C) -> %% Introduced by the occurrence of an empty `text' string in a %% `sep' context. case C of #c_fit{} -> %% Idempotency: rewrite(D, C); _ -> rewrite(D, #c_fit{c = C}) % pass on end; rewrite(#float{d = D, h = H, v = V}, C) -> case C of #c_beside{d = D1, c = C1} -> case C1 of #c_float_beside{d = D2, h = H1, v = V1, c = C2} when H1 > H -> %% Move left rewrite(beside(floating(D, H, V), beside(floating(D2, H1, V1), D1)), C2); #c_float_beside{d = D2, h = H1, v = V1, c = C2} when V1 /= V -> %% Align vertically rewrite(above(floating(D2, H1, V1), beside(floating(D, H, V), D1)), C2); #c_float_above_nest{d = D2, h = H1, v = V1, i = N1, c = C2} when V1 > V -> %% Move up (note the indentation, and note %% that all three become aligned vertically) rewrite(above(nest(N1, floating(D, H, V)), above(floating(D2, H1, V1), D1)), C2); #c_float_above_nest{d = D2, h = H1, v = V1, i = _N1, c = C2} when V1 == V, H1 /= H -> %% Align horizontally rewrite(beside(floating(D2, H1, V1), beside(floating(D, H, V), D1)), C2); _ -> rewrite(D1, #c_float_beside{d = D, h = H, v = V, c = C1}) end; #c_above_nest{d = D1, i = N, c = C1} -> case C1 of #c_float_beside{d = D2, h = H1, v = V1, c = C2} when H1 > H -> %% Move left (indentation is lost; note that %% all three become aligned horizontally) rewrite(beside(floating(D, H, V), beside(floating(D2, H1, V1), D1)), C2); #c_float_beside{d = D2, h = H1, v = V1, c = C2} when V1 /= V -> %% Align vertically rewrite(above(floating(D2, H1, V1), above(floating(D, H, V), nest(N, D1))), C2); #c_float_above_nest{d = D2, h = H1, v = V1, i = N1, c = C2} when V1 > V -> %% Move up (note the indentation) rewrite(above(nest(N1, floating(D, H, V)), above(floating(D2, H1, V1), nest(N + N1, D1))), C2); #c_float_above_nest{d = D2, h = H1, v = V1, i = _N1, c = C2} when V1 == V, H1 /= H -> %% Align horizontally rewrite(beside( floating(D2, H1, V1), above(floating(D, H, V), nest(N, D1))), C2); _ -> rewrite(D1, #c_float_above_nest{d = D, h = H, v = V, i = N, c = C1}) end; #c_fit{c = C1} -> rewrite(floating(fit(D), H, V), C1); #c_float_beside{d = D1, h = H1, v = V1, c = C1} -> if H1 > H -> %% Swap rewrite(beside(floating(D, H, V), floating(D1, H1, V1)), C1); V1 /= V -> %% Align vertically rewrite(above(floating(D, H, V), floating(D1, H1, V1)), C1); true -> %% Drop the 'float' wrapper of the rightmost. rewrite(beside(floating(D1, H1, V1), D), C1) end; #c_float_above_nest{d = D1, h = H1, v = V1, i = N, c = C1} -> if V1 > V -> %% Swap (note the indentation) rewrite(above(nest(N, floating(D, H, V)), floating(D1, H1, V1)), C1); V1 == V, H1 /= H -> %% Align horizontally rewrite(beside(floating(D, H, V), floating(D1, H1, V1)), C1); true -> %% Drop the 'float' wrapper of the lower. rewrite(above(floating(D1, H1, V1), nest(N, D)), C1) end; _ -> %% All other cases simply drop the `float' wrapper. rewrite(D, C) end; rewrite(null, C) -> case C of #c_best_nest{} -> null; % done #c_above_nest{d = D, i = N, c = C1} -> rewrite(nest(N, D), C1); #c_beside{d = D, c = C1} -> rewrite(D, C1); #c_text_beside{s = S, c = C1} -> rewrite(mktext(S), C1); #c_sep_nest{} -> %% In a `nest' context, an empty document behaves like %% the empty string. rewrite(nil(), C); #c_best_nest_or{w = W, r = R, i = N} -> %% An empty document as "nice" as it can be, so we %% discard the alternative. rewrite(null, #c_best_nest{w = W, r = R, i = N}); #c_fit{c = C1} -> rewrite(null, C1); % identity #c_float_beside{d = D, h = _H, v = _V, c = C1} -> %% We just remove the float wrapper; cf. below. rewrite(beside(D, null), C1); #c_float_above_nest{d = D, h = _H, v = _V, i = N, c = C1} -> %% It is important that this case just removes the %% float wrapper; the empty document must be preserved %% until later, or it will not be useful for forcing %% line breaks. rewrite(above(D, nest(N, null)), C1) end. %% Both `null' and `empty' are already in use, so what do you do? nil() -> text(""). hspace() -> text([$\s]). union(D1, D2) -> #union{d1 = D1, d2 = D2}. fit(D) -> #fit{d = D}. enter_sep(Ds, N, P, C) -> case Ds of [D] -> rewrite(D, C); % Handle this case separately [D | Ds1] -> %% Note that we never build a `sep'-context with an %% empty "tail" list! `Ds1' is nonempty here! rewrite(D, #c_sep_nest{ds = Ds1, i = N, p = P, c = C}) end. %% When we expand a `sep', the extra indentation appears as `nest' %% operators, but not until then. expand_sep(D, Ds, N) -> union(fit(horizontal([D | Ds])), vertical([D | [nest(N, D1) || D1 <- Ds]])). expand_par(D, [D1 | Ds] = DL, N) -> union(beside(fit(D), beside(hspace(), mksep([fit(D1) | Ds], N - 1, true))), above(D, nest(N, par(DL)))). horizontal(Ds) -> foldr1(fun (D1, D2) -> beside(D1, beside(hspace(), D2)) end, Ds). vertical(Ds) -> foldr1(fun above/2, Ds). foldr1(_F, [H]) -> H; foldr1(F, [H | T]) -> F(H, foldr1(F, T)). %% Internal representation of strings; stores the field width and does %% not perform list concatenation until the text is requested. Strings %% are thus deep lists whose first element is the length of the string. %% Null strings are strings whose "official width" is zero, typically %% used for markup that is not supposed to affect the indentation. string(S) -> [strwidth(S) | S]. null_string(S) -> [0 | S]. concat([_ | []], [_ | _] = S) -> S; concat([_ | _] = S, [_ | []]) -> S; concat([L1 | S1], [L2 | S2]) -> [L1 + L2 | [S1 | S2]]. string_chars([_ | S]) -> S. width(S) -> hd(S). is_empty_string([_ | []]) -> true; is_empty_string([_ | _]) -> false. %% We need to use `strwidth' instead of list `length', to properly %% handle Tab characters in the text segments. Note that the width of %% tabs is hard-coded as 8 character positions, and that strings are %% individually considered to be aligned at column 0; Tab characters are %% not really nice to give to a prettyprinter, and this seems to be the %% best interpretation. strwidth(S) -> strwidth(S, 0). strwidth([$\t | Cs], N) -> strwidth(Cs, N - (N rem 8) + 8); strwidth([_ | Cs], N) -> strwidth(Cs, N + 1); strwidth([], N) -> N. %% =====================================================================