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

Adds flag statement (TODO, FIXME, ...) parsing for docs #3519

Merged
merged 2 commits into from
Nov 20, 2016

Conversation

samueleaton
Copy link
Contributor

@samueleaton samueleaton commented Nov 7, 2016

Adds parsing for flag statements (TODO, FIXME, BUG, ...) for docs as discussed in issue #3460

I had to create a workaround because TODO: and other keywords are not part of the markdown parser.

The code I added:
(1) Makes sure that lines starting with TODO: are isolated into their own paragraph
(2) Splits the HTML output by paragraph, and then replaces TODO: with the relevant HTML

If there is another way to do this (e.g. with the markdown parser) point me in the right direction and I'll see what I can do.

Screenshot

screen shot 2016-10-23 at 10 18 17 am

@oprypin
Copy link
Member

oprypin commented Nov 7, 2016

What is the point of this?

@Sija
Copy link
Contributor

Sija commented Nov 7, 2016

@BlaXpirit @samueleaton explained his reasoning behind this PR in #3460.
@samueleaton I'd add support for FIXME too.

@ysbaddaden
Copy link
Contributor

As well as NOTE and OPTIMIZE.

@samueleaton
Copy link
Contributor Author

samueleaton commented Nov 8, 2016

Should TODO, NOTE, FIXME, and OPTIMIZE be different colors or all orange?

Different Colors:

screen shot 2016-11-07 at 6 06 09 pm

All Orange:

screen shot 2016-11-07 at 6 07 14 pm

EDIT: I'm on a little macbook air with a crappy screen. I'll make sure the colors are good when I'm on my good monitor

@keplersj
Copy link
Contributor

keplersj commented Nov 8, 2016

I like the different color! Perhaps also add support for DEPRECATED?

@samueleaton
Copy link
Contributor Author

@keplersj If we were to do DEPRECATED, would it be the same format with a colon? Would the text that follows the colon be an explanation on why it is deprecated?

Like this:

# one of our beloved customers
#
# TODO: create methods for this class
#
class Customer

  # fetches a customer from the database by id
  #
  # DEPRECATED: Use *retrieve* instead
  # TODO: query db for customer  
  # TODO: return nil if customer not found with id  
  # TODO: return an instance of `Customer` if customer exists  
  def self.fetch(customer_id : String)
  end
end

I personally think deprecated should be on the same line as the method description:

# one of our beloved customers
#
# TODO: create methods for this class
#
class Customer

  # fetches a customer from the database by id (DEPRECATED)
  #
  # TODO: query db for customer  
  # TODO: return nil if customer not found with id  
  # TODO: return an instance of `Customer` if customer exists  
  def self.fetch(customer_id : String)
  end
end

This way the deprecated flag would be next to the method definition rather than underneath it. It would also make it so you can't put multiple DEPRECATED flags, which seems appropriate.

@keplersj
Copy link
Contributor

keplersj commented Nov 8, 2016

@samueleaton I like the first one better, I think it's a lot like Scala's deprecated annotation.

@samueleaton
Copy link
Contributor Author

samueleaton commented Nov 8, 2016

As of the latest commit, the following will be parsed in the docs:

  • Red flags: FIXME, BUG, DEPRECATED
  • Orange flags: TODO
  • Yellow flags: NOTE
  • Green flags: OPTIMIZE

@samueleaton
Copy link
Contributor Author

BUG and FIXME are seem similar. Is there one that people are more fond of? I am in favor of BUG. Its shorter and is a more common word (easier to remember).

@ysbaddaden
Copy link
Contributor

FIXME is more common. It may no be a bug, something poorly written for example.

@@ -241,6 +251,17 @@ class Crystal::Doc::Generator
end
end

# adds extra line break to TODO lines
def isolate_flag_lines(string)
string.split(/\n/).map do |line|
Copy link
Contributor

Choose a reason for hiding this comment

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

String.build do |io|
  string.each_line.join('\n', io) do |line|
    if line =~ /^ ?(TODO|FIXME|BUG|NOTE|OPTIMIZE|DEPRECATED):/
      io << line << '\n'
    else
      io << line
    end
  end
end

Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like the regex here and the regex on line 235-240 could be replaced by a method which takes a line and returns {flag, text} or nil. We can even combine them, as HTML passes through markdown unimpeded.

For example:

if flag, text = parse_flag(line)
  io << %(<span class="flag flag-#{flag.downcase}">#{flag}</span> #{text}\n)
else
  io << line
end

which requires only one pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you saying to remove the regex completely or just to consolidate the regex into a single method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@RX14 I remember why I had to do the regex twice (let me know if your solution would still apply here).

If I were to replace the TODO: with the HTML before the line is parsed, then the markdown parser will escape the HTML. But if I replace the TODO: with HTML after the parser, then each TODO: statement is not guaranteed to be in its own paragraph.

That is why:

  • before the markdown parser I am adding an extra \n after each TODO: line (to guarantee that each line is in its own paragraph)
  • after the markdown parser I am replacing the TODO: with the HTML (because the HTML would be escaped otherwise)

Copy link
Contributor

Choose a reason for hiding this comment

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

I was assuming that html passed through the markdown parser unescaped, because that's what I've seen most engines do. Github lets you use certain html tags in markdown.

I think that it is probably a bad default to let html through, but it should at least be an option. However if we went this route we would have to sanitize html to include only certain tags (github has a list they let through their markdown). It might be worth looking at nature markdown implementations to see how this is done.

Copy link
Contributor

Choose a reason for hiding this comment

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

However my first suggestion with the each_line and join with block are still valid with this workaround.

@samueleaton
Copy link
Contributor Author

samueleaton commented Nov 8, 2016

@ysbaddaden Two Questions: (1) Do we want to require that every flag ends in a colon? (2) Do we want all flags to be required to be uppercase?

Ending in Colon

For example, here the DEPRECATED would become a flag:

class Customer

  # fetches a customer from the database by id
  #
  # DEPRECATED: This method was buggy and is deprecated 
  def self.fetch(customer_id : String)
  end
end

But here the DEPRECATED would not become a flag:

class Customer

  # fetches a customer from the database by id
  #
  # DEPRECATED
  def self.fetch(customer_id : String)
  end
end

Require Uppercase

Do we want this to work?

class Customer

  # fetches a customer from the database by id
  #
  # deprecated: This method was buggy and is deprecated 
  def self.fetch(customer_id : String)
  end
end

@samueleaton samueleaton changed the title Adds TODO statement parsing for docs Adds flag statement (TODO, FIXME, ...) parsing for docs Nov 8, 2016
Copy link
Contributor

@Sija Sija left a comment

Choose a reason for hiding this comment

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

Both FIXME and BUG should be supported, there's no need to chose one over the other.

@samueleaton
Copy link
Contributor Author

@Sija Gotcha. I reverted the commit.

@ysbaddaden
Copy link
Contributor

No big rule, but I would say that the colon is optional, as long as the definition is on its own line, whereas uppercase is mandatory.

That is:

TODO
TODO incredible feature
TODO: support ARM

@samueleaton
Copy link
Contributor Author

I updated the code to also flag the keywords in the main project README.

Before
readme-before

After
readme-after

end.join("<p>")
end

# Adds extra line break to flag keyword lines
def isolate_flag_lines(string)
string.split(/\n/).map do |line|
if line =~ /^ ?(TODO|FIXME|BUG|NOTE|OPTIMIZE|DEPRECATED):?/
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd use a hash containing flags and corresponding colors — or even just an array with flags, moving the colors into the css itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Sija Something like this?

FLAG_COLORS = {
  "BUG" => "red",
  "DEPRECATED" => "red",
  "FIXME" => "red",
  "NOTE" => "yellow",
  "OPTIMIZE" => "green",
  "TODO" => "orange"
}
FLAGS = FLAG_COLORS.keys

def generate_flags(string)
  FLAGS.reduce(string) do |str, flag|
    flag_regexp = /<p>\s*#{flag}(:| )?/
    element_sub = %(<p><span class="flag #{FLAG_COLORS[flag]}">#{flag}</span> )
    str.gsub(flag_regexp, element_sub)
  end
end

Copy link
Contributor

Choose a reason for hiding this comment

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

yep, and you could reuse it in isolate_flag_lines method too.

@samueleaton
Copy link
Contributor Author

Here is the latest on colors:

flag_colors

@luislavena
Copy link
Contributor

@samueleaton seems that the failure in Travis is caused by formatting issues. Can you reformat tools/doc/generator.cr using the compiler from the checkout (not the 0.19.4 release) and push again?

Thank you.

@@ -226,9 +237,11 @@ class Crystal::Doc::Generator
end

def doc(context, string)
String.build do |io|
Markdown.parse string, MarkdownDocRenderer.new(context, io)
str = isolate_flag_lines string
Copy link
Contributor

Choose a reason for hiding this comment

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

why create new variable? couldn't that just be string = isolate_flag_lines string?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Been doing some functional programming and it is showing 😅 . I can change 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.

I could probably just remove that line altogether and do this

Markdown.parse isolate_flag_lines(string), MarkdownDocRenderer.new(context, io)

Copy link
Contributor

Choose a reason for hiding this comment

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

Keeping it in a separate line seems clearer to me.

padding: 2px 4px 1px;
border-radius: 3px;
margin-right: 3px;
font-size: 11px;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd suggest folding multiple border declarations into:

border: 1px solid transparent;

and l8r in span.flag.<color>:

border-color: #color;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This came to mind but I i didn't think it was necessary. In hindsight it is better that way. Good eye. 👁 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

🍪

@@ -52,7 +63,7 @@ class Crystal::Doc::Generator
Markdown.parse body, MarkdownDocRenderer.new(program_type, io)
end

File.write "#{@dir}/index.html", MainTemplate.new(body, types, repository_name)
File.write "#{@dir}/index.html", MainTemplate.new(generate_flags(body), types, repository_name)
Copy link
Contributor

@RX14 RX14 Nov 10, 2016

Choose a reason for hiding this comment

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

I actually thing that instead of duplicating changes in this method, this can be replaced with a call to doc(program_type, body), where body is the old definition.

Copy link
Contributor

Choose a reason for hiding this comment

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

This hasn't been fixed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed it. I even made an optimization where it won't even run the markdown parser on an empty string for the README if the file doesn't exist.

@Sija
Copy link
Contributor

Sija commented Nov 12, 2016

GTG?

@luislavena
Copy link
Contributor

I think is good enough to be merged, @samueleaton can you rebase and squash all the changes?

Thank you.

@asterite
Copy link
Member

O still don't quite understand why you wouldn't put a big TODO in public documentation. Is it to encourage pull requests? Is it a remainder for self? Is it to make a library look incomplete?

I'm not against this change, I just can't see the usefulness.

@luislavena
Copy link
Contributor

Hello @asterite I'm not sure to follow:

why you wouldn't put a big TODO in public documentation

You mean the opposite?

Since docs of your project (public or private) will be processed by crystal doc, I personally find it helpful to highlight any of these notes in our methods or classes.

That of course doesn't cancel or render pointless to have a TODO section on the README file or even a ROADMAP.md document.

I don't think they are mutually exclusive.

@samueleaton
Copy link
Contributor Author

@asterite its squashed and the reviews are fixed. You will also need to merge the pending request at crystal-book #45.

body = String.build do |io|
Markdown.parse body, MarkdownDocRenderer.new(program_type, io)
end
body = if filename
Copy link
Contributor

Choose a reason for hiding this comment

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

I much prefer the old style of having body = inside the block. Also isn't this String.build redundant? Doesn't doc return a string anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed. although I would have done...

body = filename ? doc(program_type, File.read(filename)) : ""

... but i don't think it would have been received well

@Sija
Copy link
Contributor

Sija commented Nov 19, 2016

@asterite :shipit:

@asterite
Copy link
Member

Is this ready to merge? If so, I'll merge it

@Sija
Copy link
Contributor

Sija commented Nov 19, 2016

Seems like it :)

@RX14
Copy link
Contributor

RX14 commented Nov 19, 2016

I can't find anything to complain about any more, at least :)

Maybe if we hit performance issues in the doc generator the regex can be replaced but I think it's a non-issue for now.

@asterite
Copy link
Member

I just tried it and the resulting doc is broken. There are double new lines everywhere. See screenshot:

screenshot 2016-11-19 21 21 54

So for now I won't merge this.

@samueleaton
Copy link
Contributor Author

@asterite I found and fixed the problem. I expected string.each_line.join("", io) do |line, io| to remove the \n at the end of each line in the block. (as would happen with string.split("\n").each do |line|).

@asterite
Copy link
Member

Looks good now, thank you! ❤️

@asterite asterite merged commit 1928ce5 into crystal-lang:master Nov 20, 2016
@asterite asterite added this to the 0.20.0 milestone Nov 20, 2016
@Sija
Copy link
Contributor

Sija commented Dec 2, 2016

Multiline flag statements ...

# NOTE: This some rather long long long long long
# note split to more lines.

... end up like this (which probably is a bug):

image

@RX14
Copy link
Contributor

RX14 commented Dec 2, 2016

@Sija I think that that is correct. Your example looks like 2 lines to me. However there should be a way to have multi line todos, maybe indent?

@Sija
Copy link
Contributor

Sija commented Dec 2, 2016

@RX14 I think that particular paragraphs and code blocks are supposed to be surrounded by an empty line, which should allow you for writing multiline statements without additional indentation.

# Multiline comments have all lines
# concatenated until empty line is found.
#
# This would be next paragraph.

@RX14
Copy link
Contributor

RX14 commented Dec 2, 2016

So what's the difference between these?

# TODO this is a long string which is a
# TODO which wraps into another line.
# TODO this is todo 1
# TODO this is todo 2

I meant to first one to render "this is a long string which is a TODO which wraps into another line." and the second to render 2 TODOs.

@samueleaton
Copy link
Contributor Author

samueleaton commented Dec 2, 2016

@Sija That is how it was intended. But I agree with @RX14 that there should be support for multiline flags.

However, I would think that users are more likely to have 2 or more flags in a row than have a really long flag, so I don't think a set of flags should have an extra blank line between each flag.

@Sija
Copy link
Contributor

Sija commented Dec 2, 2016

I agree that TODO flags are rather short, but NOTE ones easily could span far more than 80 characters which is often used as a soft wrap limit. I also agree that every flag separated by an extra
newline would be lengthy. Indentation seems like a good way to handle that.

firejox pushed a commit to firejox/crystal that referenced this pull request Dec 12, 2016
@samueleaton
Copy link
Contributor Author

samueleaton commented Dec 16, 2016

@Sija I'm working on a project and now agree with you about the NOTEs extending more than a single line. 😭

So I think I'll make another PR with the following change: Only add an extra line break to a flag line if the next line is also a flag line. Just move the line break to the front of a flag line. simple.

Example

# My Class
#
# NOTE: Xxxxxx xxxxx xx xxxxxxx x xx.
# NOTE: Yyyyyy y yyy y y yyy yyy yy yyyyy yyyy
# yyy yyyyy y yyyy y yyyyy yyyyyyy yy yy yyyy yy
# yyyyyyyyy yyyyyyyy y yy yy yyyy y.
# NOTE: Zzzzz zzzzzz zzz zzzzzz.
#
# Qqqq qq q q q qqqqq qqq q qqqq qq q qqq q qqqq q qqqq q.
class MyClass
end

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.

8 participants