UNPKG

coffee-formatter

Version:
324 lines (188 loc) 17.7 kB
<!DOCTYPE html> <html> <head> <title>Coffee-Formatter</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <link rel="stylesheet" media="all" href="public/stylesheets/normalize.css" /> <link rel="stylesheet" media="all" href="docco.css" /> </head> <body> <div class="container"> <div class="page"> <div class="header"> <h1 id="coffee-formatter">Coffee-Formatter</h1> <h2 id="introduction">Introduction</h2> <p>CoffeeFormatter (abbreviated as CF) is a, guess what, formatter for CoffeeScript. Let me know if you were actually expecting otherwise lol.</p> <p>The code is written in Literate CoffeeScript. Checkout Wikipedia for what “Literate Programming” means.</p> <h2 id="code">Code</h2> <h3 id="dependencies">Dependencies</h3> <p>CF relies on the following packages:</p> <ul> <li><code>lazy</code> for reading files line by line. An example is given <a href="http://stackoverflow.com/questions/6156501/read-a-file-one-line-at-a-time-in-node-js">here</a>.</li> <li><code>fs</code> for file io.</li> <li><code>optimist</code> for command line parsing.</li> </ul> <p>Code:</p> <div class='highlight'><pre>Lazy = <span class="hljs-built_in">require</span> <span class="hljs-string">'lazy'</span> fs = <span class="hljs-built_in">require</span> <span class="hljs-string">'fs'</span></pre></div> </div> <p>By default, we use a tab width of 2 and use spaces exclusively. This is the style most widely used in the community. For a detailed guide of CoffeeScript style, check out <a href="https://github.com/polarmobile/coffeescript-style-guide">this</a>.</p> <div class='highlight'><pre>argv = (<span class="hljs-built_in">require</span> <span class="hljs-string">'optimist'</span>) .<span class="hljs-reserved">default</span>(<span class="hljs-string">'tab-width'</span>, <span class="hljs-number">2</span>) .<span class="hljs-reserved">default</span>(<span class="hljs-string">'use-space'</span>, <span class="hljs-literal">true</span>) .argv</pre></div> <h3 id="constants">Constants</h3> <p>We define a set of constants, including:</p> <p>Two-space operators. These operators should have one space both before and after.</p> <div class='highlight'><pre>TWO_SPACE_OPERATORS = [<span class="hljs-string">'?='</span>, <span class="hljs-string">'='</span>, <span class="hljs-string">'+='</span>, <span class="hljs-string">'-='</span>, <span class="hljs-string">'=='</span>, <span class="hljs-string">'&lt;='</span>, <span class="hljs-string">'&gt;='</span>, <span class="hljs-string">'!'</span>, <span class="hljs-string">'&gt;'</span>, <span class="hljs-string">'&lt;'</span>, <span class="hljs-string">'+'</span>, <span class="hljs-string">'-'</span>, <span class="hljs-string">'*'</span>, <span class="hljs-string">'/'</span>, <span class="hljs-string">'%'</span>]</pre></div> <p>One-space operators. They should have one space after.</p> <div class='highlight'><pre>ONE_SPACE_OPERATORS = [<span class="hljs-string">':'</span>, <span class="hljs-string">'?'</span>, <span class="hljs-string">')'</span>, <span class="hljs-string">'}'</span>, <span class="hljs-string">','</span>]</pre></div> <h3 id="helper-functions">Helper Functions</h3> <p>Given a line and an index, the function determines whether or not the index is inside of a CoffeeScript string or part of a CoffeeScript comment.</p> <div class='highlight'><pre><span class="hljs-function"><span class="hljs-title">inStringOrComment</span> = <span class="hljs-params">(index, line)</span> -&gt;</span> <span class="hljs-keyword">for</span> c, i <span class="hljs-keyword">in</span> line <span class="hljs-keyword">if</span> c == <span class="hljs-string">'#'</span> <span class="hljs-keyword">and</span> i &lt;= index <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span> <span class="hljs-keyword">if</span> c == <span class="hljs-string">"'"</span> <span class="hljs-keyword">or</span> c == <span class="hljs-string">'"'</span> subLine = line.substr (i + <span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> cc, ii <span class="hljs-keyword">in</span> subLine <span class="hljs-keyword">if</span> cc == c <span class="hljs-keyword">if</span> i &lt;= index &lt;= (ii + i + <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span> <span class="hljs-keyword">else</span> <span class="hljs-keyword">return</span> inStringOrComment (index - (ii + <span class="hljs-number">1</span>)), (line.substr (ii + <span class="hljs-number">1</span>))</pre></div> <p>Match regex</p> <div class='highlight'><pre> <span class="hljs-keyword">if</span> c == <span class="hljs-string">"/"</span> subLine = line.substr (i + <span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> cc, ii <span class="hljs-keyword">in</span> subLine <span class="hljs-keyword">if</span> cc == <span class="hljs-string">" "</span> <span class="hljs-keyword">continue</span> <span class="hljs-keyword">if</span> cc == c <span class="hljs-keyword">if</span> i &lt;= index &lt;= (ii + i + <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span> <span class="hljs-keyword">else</span> <span class="hljs-keyword">return</span> inStringOrComment (index - (ii + <span class="hljs-number">1</span>)), (line.substr (ii + <span class="hljs-number">1</span>)) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span></pre></div> <p>The negation:</p> <div class='highlight'><pre><span class="hljs-function"><span class="hljs-title">notInStringOrComment</span> = <span class="hljs-params">(index, line)</span> -&gt;</span> <span class="hljs-keyword">return</span> <span class="hljs-keyword">not</span> inStringOrComment(index, line)</pre></div> <p><code>getExtension()</code> returns the extension of a filename, excluding the dot.</p> <div class='highlight'><pre><span class="hljs-function"><span class="hljs-title">getExtension</span> = <span class="hljs-params">(filename)</span> -&gt;</span> i = filename.lastIndexOf <span class="hljs-string">'.'</span> <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> i &lt; <span class="hljs-number">0</span> <span class="hljs-keyword">then</span> <span class="hljs-string">''</span> <span class="hljs-keyword">else</span> filename.substr (i+<span class="hljs-number">1</span>)</pre></div> <p>This function makes sure that there is one and only one space before and after the operators defined in <code>TWO_SPACE_OPERATORS</code>. Before it inserts spaces, however, it makes sure that the operator in question is not part of a string.</p> <p>The idea behind this implementation is that, we can firstly add one space both before and after the operator, and then use <code>shortenSpaces</code> (described later) to get rid of any additional spaces.</p> <p>The boolean logic is much more complex than I would like. It should be refactored at some point.</p> <div class='highlight'><pre><span class="hljs-function"><span class="hljs-title">formatTwoSpaceOperator</span> = <span class="hljs-params">(line)</span> -&gt;</span> <span class="hljs-keyword">for</span> operator <span class="hljs-keyword">in</span> TWO_SPACE_OPERATORS newLine = <span class="hljs-string">''</span> skipNext = <span class="hljs-literal">false</span> <span class="hljs-keyword">for</span> c, i <span class="hljs-keyword">in</span> line</pre></div> <p>Test if the operator is at i</p> <div class='highlight'><pre> <span class="hljs-keyword">if</span> (line.substr(i).indexOf(operator) == <span class="hljs-number">0</span>) <span class="hljs-keyword">and</span> (notInStringOrComment i, line) <span class="hljs-keyword">and</span> (<span class="hljs-keyword">not</span> ((operator.length == <span class="hljs-number">1</span>) <span class="hljs-keyword">and</span> ((line[i + <span class="hljs-number">1</span>] <span class="hljs-keyword">in</span> TWO_SPACE_OPERATORS) <span class="hljs-keyword">or</span> (line[i-<span class="hljs-number">1</span>] <span class="hljs-keyword">in</span> TWO_SPACE_OPERATORS)))) newLine += <span class="hljs-string">" <span class="hljs-subst">#{operator}</span> "</span> <span class="hljs-comment"># Insert a space before and after</span> skipNext = <span class="hljs-literal">true</span> <span class="hljs-keyword">if</span> operator.length == <span class="hljs-number">2</span> <span class="hljs-keyword">else</span> <span class="hljs-keyword">unless</span> skipNext newLine += c skipNext = <span class="hljs-literal">false</span> line = shortenSpaces newLine <span class="hljs-keyword">return</span> line</pre></div> <p>This method shortens consecutive spaces into one single space.</p> <div class='highlight'><pre><span class="hljs-function"><span class="hljs-title">formatOneSpaceOperator</span> = <span class="hljs-params">(line)</span> -&gt;</span> <span class="hljs-keyword">for</span> operator <span class="hljs-keyword">in</span> ONE_SPACE_OPERATORS newLine = <span class="hljs-string">''</span> <span class="hljs-keyword">for</span> c, i <span class="hljs-keyword">in</span> line thisCharAndNextOne = line.substr(i, <span class="hljs-number">2</span>) <span class="hljs-keyword">if</span> (line.substr(i).indexOf(operator) == <span class="hljs-number">0</span>) <span class="hljs-keyword">and</span> (notInStringOrComment i, line) <span class="hljs-keyword">and</span></pre></div> <p>One exception has to be accounted for, which is expression of the form <code>Object::property</code></p> <div class='highlight'><pre> (line.substr(i).indexOf(<span class="hljs-string">'::'</span>) != <span class="hljs-number">0</span>) <span class="hljs-keyword">and</span> (line.substr(i-<span class="hljs-number">1</span>).indexOf(<span class="hljs-string">'::'</span>) != <span class="hljs-number">0</span>) <span class="hljs-keyword">and</span></pre></div> <p>Another exception: <code>if (options = arguments[i])?</code></p> <div class='highlight'><pre> (line.substr(i+<span class="hljs-number">1</span>).indexOf(<span class="hljs-string">'?'</span>) != <span class="hljs-number">0</span>) <span class="hljs-keyword">and</span></pre></div> <p>And also “),” “).” “)[“ and “))” shouldn’t be separated by space:</p> <div class='highlight'><pre> (thisCharAndNextOne <span class="hljs-keyword">isnt</span> <span class="hljs-string">"),"</span>) <span class="hljs-keyword">and</span> (thisCharAndNextOne <span class="hljs-keyword">isnt</span> <span class="hljs-string">")("</span>) <span class="hljs-keyword">and</span> (thisCharAndNextOne <span class="hljs-keyword">isnt</span> <span class="hljs-string">")."</span>) <span class="hljs-keyword">and</span> (thisCharAndNextOne <span class="hljs-keyword">isnt</span> <span class="hljs-string">")["</span>) <span class="hljs-keyword">and</span> (thisCharAndNextOne <span class="hljs-keyword">isnt</span> <span class="hljs-string">"))"</span>) newLine += <span class="hljs-string">"<span class="hljs-subst">#{operator}</span> "</span> <span class="hljs-comment"># Insert a space after</span> <span class="hljs-keyword">else</span> newLine += c line = shortenSpaces newLine <span class="hljs-keyword">return</span> line</pre></div> <p>Note that the function should not shorten indentations.</p> <div class='highlight'><pre><span class="hljs-function"><span class="hljs-title">shortenSpaces</span> = <span class="hljs-params">(line)</span> -&gt;</span> <span class="hljs-function"> <span class="hljs-title">trimTrailing</span> = <span class="hljs-params">(str)</span> -&gt;</span> str.replace <span class="hljs-regexp">/\s\s*$/</span>, <span class="hljs-string">""</span> prevChar = <span class="hljs-literal">null</span> newLine = <span class="hljs-string">''</span> <span class="hljs-keyword">for</span> c, i <span class="hljs-keyword">in</span> line <span class="hljs-keyword">if</span> c == <span class="hljs-string">' '</span> newLine += c <span class="hljs-keyword">else</span> line = line.substring(i) <span class="hljs-keyword">break</span> <span class="hljs-keyword">for</span> c, i <span class="hljs-keyword">in</span> line <span class="hljs-keyword">unless</span> notInStringOrComment(i, line) <span class="hljs-keyword">and</span> (c == <span class="hljs-string">' '</span> == prevChar) newLine = newLine + c prevChar = c <span class="hljs-keyword">return</span> trimTrailing newLine</pre></div> <h3 id="body">Body</h3> <p>The body of this module does the following:</p> <ol> <li>Parse command line.</li> <li>Read the files specified by the user.</li> <li>Perform formatting.</li> </ol> <p>We loop through <code>argv._</code>, which should be a list of filenames given by the user.</p> <div class='highlight'><pre><span class="hljs-keyword">for</span> filename <span class="hljs-keyword">in</span> argv._</pre></div> <p>Then we check if the extension is “coffee”. Literate CoffeeScript will also be supported at some point.</p> <div class='highlight'><pre> <span class="hljs-keyword">if</span> (getExtension filename) <span class="hljs-keyword">isnt</span> <span class="hljs-string">'coffee'</span> <span class="hljs-built_in">console</span>.log <span class="hljs-string">"<span class="hljs-subst">#{filename}</span> doesn't appear to be a CoffeeScript file. Skipping..."</span> <span class="hljs-built_in">console</span>.log <span class="hljs-string">"Use --force to format it anyway."</span></pre></div> <p>If the extension is “coffee”, we proceed to the actual formatting.</p> <p>Firstly, we read the file line by line:</p> <div class='highlight'><pre> <span class="hljs-keyword">else</span> file = <span class="hljs-string">''</span> lazy = <span class="hljs-keyword">new</span> Lazy(fs.createReadStream filename, <span class="hljs-attribute">encoding</span>: <span class="hljs-string">'utf8'</span>) lazy.<span class="hljs-literal">on</span> <span class="hljs-string">'end'</span>, <span class="hljs-function">-&gt;</span> fs.writeFileSync filename, file lazy.lines .forEach (line) -&gt; line = String(line)</pre></div> <p>For some weird reason regarding IO, empty line is read as ‘0’. Therefore I have to check against 0. This may cause weird bugs if a line actually contains only ‘0’.</p> <div class='highlight'><pre> <span class="hljs-keyword">if</span> line != <span class="hljs-string">'0'</span> newLine = line</pre></div> <p><code>newLine</code> is used to hold a processed line. <code>file</code> is used to hold the processed file.</p> <p>Now we add spaces before and after a binary operator, using the helper function:</p> <div class='highlight'><pre> newLine = formatTwoSpaceOperator newLine</pre></div> <p>Do the same for single-space operators:</p> <div class='highlight'><pre> newLine = formatOneSpaceOperator newLine</pre></div> <p>Shorten any consecutive spaces into a single space:</p> <div class='highlight'><pre> newLine = shortenSpaces newLine file += newLine + <span class="hljs-string">'\n'</span> <span class="hljs-keyword">else</span> file += <span class="hljs-string">'\n'</span></pre></div> <p>After the <code>forEach</code> completes, we have a file that is formatted line by line. However, a comprehensive formatter needs to consider the code in a block level. Specifically:</p> <ul> <li>Indentation should be formatted according to the parameters specified by the user.</li> </ul> <p>This haven’t been implemented yet.</p> <h3 id="exports">Exports</h3> <h4 id="test-only">Test Only</h4> <p>The following exports are for testing only and should be commented out in production:</p> <div class='highlight'><pre>exports.shortenSpaces = shortenSpaces exports.formatTwoSpaceOperator = formatTwoSpaceOperator exports.notInStringOrComment = notInStringOrComment exports.formatOneSpaceOperator = formatOneSpaceOperator</pre></div> <div class="fleur">h</div> </div> </div> </body> </html>