coffee-formatter
Version:
A CoffeeScript formatter
324 lines (188 loc) • 17.7 kB
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">'<='</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>, <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> -></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 <= 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 <= index <= (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 <= index <= (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> -></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> -></span>
i = filename.lastIndexOf <span class="hljs-string">'.'</span>
<span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> i < <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> -></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> -></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> -></span>
<span class="hljs-function"> <span class="hljs-title">trimTrailing</span> = <span class="hljs-params">(str)</span> -></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">-></span>
fs.writeFileSync filename, file
lazy.lines
.forEach (line) ->
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>