Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[WIP] [Console] Pretty word wrapping #30590

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

Closed
wants to merge 17 commits into from
Closed

[WIP] [Console] Pretty word wrapping #30590

wants to merge 17 commits into from

Conversation

fchris82
Copy link
Contributor

@fchris82 fchris82 commented Mar 17, 2019

Q A
Branch? 4.2
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #30519
License MIT
Doc PR symfony/symfony-docs#11190

This new feature was born from a bugfix. This feature add a PrettyWordWrapping class that you can use in console, when you want to wrap the text.

@nicolas-grekas nicolas-grekas added this to the next milestone Mar 17, 2019
@fchris82 fchris82 marked this pull request as ready for review March 19, 2019 16:10
@fchris82
Copy link
Contributor Author

@ro0NL Let's discuss! :) I fully refactored the code, I made a helper from it, and integrated into Table and OutputFormetter. I changed a little bit your solution, and separated "wrapping" from "formatter" class because it would have been too complex in it.

*
* @return string
*/
public function wordwrap(string $message, int $width, int $cutOption = null): string;
Copy link
Contributor

@ro0NL ro0NL Mar 19, 2019

Choose a reason for hiding this comment

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

This is new feature isnt it? To wrap a raw string with tags (unformatted still) ...

Do we really need it? As such it's a BC break, and it's the exact same reason we added a new WrappableOutputFormatterInterface in 4.2.

New features target master and thus would imply another interface. I think we should avoid that and solve the issue in formatAndWrap().

Which takes me to my 2nd point; this PR will parse the tags twice IIUC. My main motivation for "formatAndWrap" was to be able to handle it at once. I think we should patch it instead, relying on a helper if really needed for clarity.

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 was thinking about it also. I saw 3 ways:

  1. I add the CUT option parameter to the formatAndWrap() --> this causes BC break
  2. I create a new wrap function that only can wrap --> it doesn't cause BC break
  3. I leave the CUT option parameter --> it doesn't cause BC break, but it causes inflexible wrapping function

Maybe there is a 4. option: I remove wordwrap() (maybe the formatAndWrap() too), and if somebody want to wrap then call like here:

// Without wrapping
$output->getFormatter()->format($text);

// With wrapping
$output->getFormatter()->format(
    PrettyWordWrappingHelper::wordwrap($text, 120)
);

It would be clearer, but it supersede your WrappableOutputFormatterInterface because wrapping and formatting are really and fully separated (and helps to programmers changing it a custom wrapper if they want) I would prefer this.

this PR will parse the tags twice

Yes, it is true. This is a side effect, which I think is necessary. Formatting (styling?) and wrapping can enough complex separately, that is why I think we should handle it separately. It causes more maintenance and extendable code. And this will be used in command line, the performance isn't so important. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

option 5) Your "With wrapping" example to me equals formatter->formatAndWrap($text, 120) already. That's API in 4.2 we can fix, maybe even with an @internal WordwrapHelper, or public for 4.3/master.

option 6) deprecate WrappableOutputFormatterInterface in 4.3 and add the "best" API directly to OutputFormatterInterface using #28902 (which wasnt possible back then)

In general i think the wordwrap helper should be an implementation detail of the output formatter. But we need some consensus first to move forward. Given a lot of work is done here already, perhaps we should finish it :) The bug is real.

this PR will parse the tags twice

Yes, it is true. This is a side effect, which I think is necessary.

You know best :) so far i thought it seemed reasonably possible ... but it's also still flawed currently 😅 I need to look closer at this new approach to see how it differs.

Copy link
Contributor Author

@fchris82 fchris82 Mar 22, 2019

Choose a reason for hiding this comment

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

... I'm thinking about it ...

@@ -406,7 +407,12 @@ private function createBlock(iterable $messages, string $type = null, string $st
$message = OutputFormatter::escape($message);
}

$lines = array_merge($lines, explode(PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true)));
$lines = array_merge($lines, explode(PHP_EOL, PrettyWordWrapperHelper::wrap(
Copy link
Contributor

Choose a reason for hiding this comment

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

basically i started my patch by invoking formatAndWrap() here, which conceptually works.

Copy link
Contributor

@ro0NL ro0NL left a comment

Choose a reason for hiding this comment

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

Changed fixtures look sensible 👍 , but the amount of code introduced is hard to grasp. E.g. do we really need all the CUT_ options (now)?

This seems like a lot of new features combined, which doesnt make them wrong, but i'd prefer smaller PRs personally.

At this point, aren't we better of to rewrite the tag parsing as a whole? I.e. using a (real) lexer.

@javiereguiluz
Copy link
Member

I like this feature, but I'm concerned about the naming:

  • I'd remove Pretty from PrettyWordWrapperHelper because this feature is always called "Word Wrapping" ... so that should be enough. So, PrettyWordWrapperHelper -> WordWrapperHelper
  • I'd remove LONG from CUT_LONG_WORDS because words can be short and still be cut or not. So, CUT_LONG_WORDS -> CUT_WORDS
  • I don't understand what these options do: CUT_FILL_UP_MISSING, CUT_ALL.

@fchris82
Copy link
Contributor Author

About CUT options/flags

@ro0NL and @javiereguiluz Before I reply for you one by one, I would like to clarify the importance of CUT options. I started this feature because of my existing other program, where I had to print some well formatted and colorized help/description texts, which were ugly with the existing output formatting solution. These are longer texts, with some URLs.
First I wrote a custom wrapper half year ago and I used it. My plan was that I will implement this function into Symfony when I have free time. While I was writing the code, I noticed that there are 2 big different situations because of Table helper. As you can see here: https://github.com/symfony/symfony/pull/30519/files#diff-a3a53b968f8b0f15e54e6eeef311a73aR147 , first there was only one boolean "parameter" instead of CUT options: $cutLongWords. Exactly it wasn't "my idea", I would have preferred if the implemented wordwrap() function had been similar to php wordwrap() function: http://php.net/manual/en/function.wordwrap.php . So the $cutLongWords would have been same like the php $cut parameter in native wordwrap().
But the Table console helper showed me, I didn't consider "very short" lines (<10-20 chars). The base solution isn't enough "responsive" :D I started thinking about other problems too, and because I would want to create a really flexible solution, first I added a second boolean parameter, and then a third, and that was the point when I thought, it would be too much:

public function wordwrap($string, $witdth, $cutLongWords = true, $cutURLs = false, $fillUpEnd = false, ...)
{
    // ...
}

There are many other functions where there are lots of configuration options, e.g:

If you have long rows (>50-60 chars) you may not need any CUT options, but as you can see in the documentation at the CUT examples: https://github.com/symfony/symfony-docs/pull/11190/files#diff-8d05f5767d5a596b67434f76c113bb8dR80 , there are different "good" solutions, and I would like to leave the choice for the programmer, which one they want to use. Eg ($width = 4):

No cut:

Lorem
ipsum
dolor
sit
amet

Cut every words (sit and amet also):

Lore
m ip
sum
dolo
r si
t am
et

Cut only the long words, that are longer than $width (sit and amet aren't cut)

Lore
m ip
sum
dolo
r
sit
amet

Please look at the documentation too: https://github.com/symfony/symfony-docs/pull/11190/files , there are examples and after that maybe the conception will be clearer.

@fchris82
Copy link
Contributor Author

At this point, aren't we better of to rewrite the tag parsing as a whole? I.e. using a (real) lexer.

I was thinking about it also. I started to do a tokenizer/lexer, but I rejected it fast. The "grammar" is "too" simple. There are words and some simple tags. There aren't tag attributes, special characters, placeholders, etc. Yes, a lexer would be nicer, but we don't forget: this is the console block, for commands, what are used by programmers - in most cases -, and not "non-professional" users. We need a wrapper for "help/description/information" texts and for table cells. That's all.

*/
public function format($message)
public function getDefaultWrapCutOption(): int
Copy link
Contributor

Choose a reason for hiding this comment

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

not really needed is 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.

A protected property that you can only write? It looks a little bit bad concept. A property what you can only read, it could be justified, but a property without reading option, not really.

Copy link
Contributor

Choose a reason for hiding this comment

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

im skeptical about the setter too :)

setters/getters can live independently IMHO, it's not needed for every property by definition or so. If we dont have reason to add a getter/settter i would avoid it (it introduces extra state).

In this case i'd prefer passing the cut options to formatAndWrap(), i dont see a real reason to control this default behavior.

*
* @see PrettyWordWrapperHelper
*/
public function setDefaultWrapCutOption(int $defaultWrapCutOption)
Copy link
Contributor

Choose a reason for hiding this comment

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

same, where do we use it? I tend to prefer having a $cutOptions as 3rd argument of formatAndWrap(), which makes it a clear new feature for 4.3

@@ -228,13 +230,72 @@ public function setColumnWidths(array $widths)
*
* @return $this
*/
public function setColumnMaxWidth(int $columnIndex, int $width): self
public function setColumnMaxWidth(int $columnIndex, int $width, int $cutOption = null): self
{
if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) {
throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, \get_class($this->output->getFormatter())));
}

$this->columnMaxWidths[$columnIndex] = $width;
Copy link
Contributor

Choose a reason for hiding this comment

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

i would use [$width, $cutOption] to make it clear this belongs together.


return $this;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

not sure we need all this extra API, i would keep it out at first.

@fchris82
Copy link
Contributor Author

Hello @javiereguiluz ,

thank you for your comment.

  • I'd remove Pretty from PrettyWordWrapperHelper because this feature is always called "Word Wrapping" ... so that should be enough. So, PrettyWordWrapperHelper -> WordWrapperHelper

OK, it was a good idea, I have changed it :)

  • I'd remove LONG from CUT_LONG_WORDS because words can be short and still be cut or not. So, CUT_LONG_WORDS -> CUT_WORDS

There exists a CUT_WORDS option, that means: cut every words (row length: 8 chars):

Lorem ip
sum dolo
r sit amet

CUT_LONG_WORDS means: cut words that are longer, then a row, shorter words won't be cut (row length: 8 chars):

Lorem
ipsum
dolor
sit
amet
thisisav
erylongw
ord
  • I don't understand what these options do: CUT_FILL_UP_MISSING, CUT_ALL.

CUT_FILL_UP_MISSING means: every row will be same length. It would be important when the line ha background color:

Default show:

▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧
▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧
▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧
▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧
▧▧▧▧▧▧▧▧▧▧▧

With CUT_FILL_UP_MISSING (every line is same length):

▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧
▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧
▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧
▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧
▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧▧

The missing characters are spaces, and the background color will "flow to line endings".

@fchris82
Copy link
Contributor Author

fchris82 commented Mar 27, 2019

@ro0NL What if there was a <wrap> format style? I would keep the WordWrapperHelper, sometimes it could be useful, but people could use eg:

// It prints the text in a 8 char length, yellow background box
$output->writeln('<wrap=8,fillup;bg=yellow>Lorem <info>ipsum</info> dolor sit amet.</>');

And I would refactor the text "pattern" with a lexer/tokenizer.


So, what I would do:

  • Set WrappableOutputFormatterInterface to deprecated
  • Remove my changes from OutputFormatter, refactor it whit a lexer
  • Add <wrap> "tag"
  • Refactor max column width in Table (by adding <wrap> tag eg)

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

Successfully merging this pull request may close these issues.

5 participants