Created
July 29, 2013 23:30
-
-
Save andreypopp/6108804 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env coffee | |
fs = require 'fs' | |
crypto = require 'crypto' | |
{Parser, tree} = require 'less' | |
{extend, isString} = require 'underscore' | |
filename = process.argv[2] | |
parser = new Parser(filename: filename) | |
str = fs.readFileSync(filename).toString() | |
defineVisitor = (base, props) -> | |
extend Object.create(base), props | |
genVar = -> | |
"var#{crypto.randomBytes(12).toString('hex')}" | |
renderValue = (node, options) -> | |
options = extend {}, options | |
return node if isString node | |
impl = Object.create(expressionVisitor) | |
impl.options = options | |
visitor = new tree.visitor impl | |
visitor.visit node | |
if impl.value then impl.value.trim() else '' | |
renderTree = (node, indent = '') -> | |
impl = Object.create(treeVisitor) | |
impl.indent = indent | |
new tree.visitor(impl).visit(node) | |
renderMixinParam = (node) -> | |
param = node.name.slice(1) | |
if node.value | |
param = "#{param}=#{renderValue(node.value)}" | |
param | |
renderMixinArg = (node) -> | |
param = renderValue(node.value) | |
if node.name | |
param = "#{node.name.slice(1)}=#{param}" | |
param | |
renderPrelude = -> | |
console.log """ | |
lesscss-percentage(n) | |
(n * 100)% | |
""".trim() | |
toUnquoted = (value) -> | |
value | |
.replace(/@{/g, '"@{') | |
.replace(/}/g, '}"') | |
.split(/(@{)|}/) | |
.filter((v) -> v != '@{' and v != '}' and v?.length > 0) | |
.join(' + ') | |
funcMap = | |
'%': 's' | |
'percentage': 'lesscss-percentage' | |
mixinMap = | |
translate: 'mixin-translate' | |
scale: 'mixin-scale' | |
rotate: 'mixin-rotate' | |
skew: 'mixin-skew' | |
translate3d: 'mixin-translate3d' | |
baseVisitor = | |
visitAlpha: (node) -> | |
throw new Error('not implemented') | |
visitAnonymous: (node) -> | |
throw new Error('not implemented') | |
visitAssigment: (node) -> | |
throw new Error('not implemented') | |
visitAttribute: (node) -> | |
throw new Error('not implemented') | |
visitCall: (node) -> | |
throw new Error('not implemented') | |
visitColor: (node) -> | |
throw new Error('not implemented') | |
visitComment: (node) -> | |
throw new Error('not implemented') | |
visitCondition: (node) -> | |
throw new Error('not implemented') | |
visitDimension: (node) -> | |
throw new Error('not implemented') | |
visitDirective: (node) -> | |
throw new Error('not implemented') | |
visitElement: (node) -> | |
throw new Error('not implemented') | |
visitExpression: (node) -> | |
throw new Error('not implemented') | |
visitExtend: (node) -> | |
throw new Error('not implemented') | |
visitImport: (node) -> | |
throw new Error('not implemented') | |
visitJavaScript: (node) -> | |
throw new Error('not implemented') | |
visitKeyword: (node) -> | |
throw new Error('not implemented') | |
visitMedia: (node) -> | |
throw new Error('not implemented') | |
visitMixin: (node) -> | |
throw new Error('not implemented') | |
visitMixinCall: (node) -> | |
throw new Error('not implemented') | |
visitMixinDefinition: (node) -> | |
throw new Error('not implemented') | |
visitNegative: (node) -> | |
throw new Error('not implemented') | |
visitOperation: (node) -> | |
throw new Error('not implemented') | |
visitParen: (node) -> | |
throw new Error('not implemented') | |
visitQuoted: (node) -> | |
throw new Error('not implemented') | |
visitRule: (node, options) -> | |
throw new Error('not implemented') | |
visitRuleset: (node, options) -> | |
throw new Error('not implemented') | |
visitSelector: (node, options) -> | |
throw new Error('not implemented') | |
visitValue: (node) -> | |
throw new Error('not implemented') | |
visitVariable: (node) -> | |
throw new Error('not implemented') | |
visitURL: (node) -> | |
throw new Error('not implemented') | |
visitUnicodeDescriptor: (node) -> | |
throw new Error('not implemented') | |
expressionVisitor = defineVisitor baseVisitor, | |
acc: (v) -> | |
@value = '' unless @value | |
@value += ' ' + v | |
visitAnonymous: (node) -> | |
@acc node.value | |
visitDimension: (node) -> | |
@acc "#{node.value}#{node.unit.numerator.join('')}" | |
visitVariable: (node) -> | |
if @options.unquote | |
@acc "@{#{node.name.slice(1)}}" | |
else | |
@acc node.name.slice(1) | |
visitCall: (node, options) -> | |
options.visitDeeper = false | |
args = node.args.map((e) => renderValue(e, @options)).join(', ') | |
name = funcMap[node.name] or node.name | |
args = args.replace(/%d/g, '%s') if name == 's' | |
@acc "#{name}(#{args})" | |
visitSelector: (node, options) -> | |
options.visitDeeper = false | |
@acc node.elements | |
.map((e) => | |
"#{e.combinator.value}#{renderValue(e, @options)}") | |
.join('') | |
.replace(/>/g, ' > ') | |
.replace(/\+/g, ' + ') | |
visitElement: (node, options) -> | |
options.visitDeeper = false | |
@acc renderValue(node.value, @options) | |
visitAttribute: (node, options) -> | |
options.visitDeeper = false | |
rendered = node.key | |
rendered += node.op + renderValue(node.value, @options) if node.op | |
@acc "[#{rendered}]" | |
visitKeyword: (node) -> | |
@acc node.value | |
visitQuoted: (node) -> | |
if node.escaped | |
value = toUnquoted(node.value) | |
@acc "unquote(#{node.quote}#{value}#{node.quote})" | |
else | |
@acc "#{node.quote}#{node.value}#{node.quote}" | |
visitParen: (node, options) -> | |
options.visitDeeper = false | |
@acc "(#{renderValue(node.value, @options)})" | |
visitRule: (node, options) -> | |
options.visitDeeper = false | |
@acc "#{node.name}: #{renderValue(node.value, @options)}" | |
visitOperation: (node, options) -> | |
options.visitDeeper = false | |
if node.operands.length != 2 | |
throw new Error('assertion') | |
[left, right] = node.operands | |
value = "#{renderValue(left, @options)} #{node.op} #{renderValue(right, @options)}" | |
value = "(#{value})" | |
@acc value | |
visitValue: (node, options) -> | |
options.visitDeeper = false | |
@acc node.value.map((e) => renderValue(e, @options)).join(', ') | |
visitExpression: (node, options) -> | |
options.visitDeeper = false | |
@acc node.value.map((e) => renderValue(e, @options)).join(' ') | |
visitColor: (node) -> | |
if node.rgb | |
c = "rgb(#{node.rgb.join(', ')}" | |
if node.alpha | |
c += ", #{node.alpha}" | |
c += ")" | |
@acc c | |
else | |
throw new Error("unknow color #{node}") | |
visitNegative: (node) -> | |
@acc "- #{renderValue(node.value, @options)}" | |
treeVisitor = defineVisitor baseVisitor, | |
indent: '' | |
increaseIndent: -> | |
@indent + ' ' | |
decreaseIndent: -> | |
@indent.slice(0, -2) | |
p: (m, indent) -> | |
indent = indent or @indent | |
console.log "#{indent}#{m.trim()}" | |
isNamespaceDefinition: (node) -> | |
return false unless node.type == 'Ruleset' | |
return false unless node.selectors.length == 1 | |
name = renderValue node.selectors[0] | |
return false unless name[0] == '#' | |
return false unless node.rules.every (rule) -> | |
# TODO: variables are also allowed | |
rule.type == 'MixinDefinition' or rule.type == 'Comment' | |
return name.slice(1) | |
isNamespaceCall: (node) -> | |
visitRuleset: (node, options, directive = '') -> | |
unless node.root | |
namespace = @isNamespaceDefinition(node) | |
options.visitDeeper = false | |
if namespace | |
for rule in node.rules | |
# TODO: handle variables | |
if rule.type == 'MixinDefinition' | |
rule.name = ".#{namespace}-#{rule.name.slice(1)}" | |
renderTree(rule, @indent) | |
else | |
if node.rules.length > 0 | |
@p "#{directive}#{node.selectors.map(renderValue).join(', ')}" | |
for rule in node.rules | |
renderTree(rule, @increaseIndent()) | |
visitRulesetOut: (node) -> | |
unless node.root | |
@decreaseIndent() | |
visitRule: (node, options) -> | |
options.visitDeeper = false | |
name = node.name | |
if name[0] == '@' | |
@p "#{name.slice(1)} = #{renderValue(node.value)}" | |
else | |
@p "#{name} #{renderValue(node.value)}#{node.important}" | |
visitComment: (node) -> | |
@p node.value unless node.silent | |
visitMedia: (node, options) -> | |
options.visitDeeper = false | |
features = renderValue(node.features, unquote: true) | |
if /@{/.exec features | |
mediaVar = genVar() | |
@p "#{mediaVar} = \"#{toUnquoted(features)}\"" | |
@p "@media #{mediaVar}" | |
else | |
@p "@media #{features}" | |
for rule in node.ruleset.rules | |
renderTree(rule, @increaseIndent()) | |
visitSelector: (node, options) -> | |
options.visitDeeper = false | |
@p node.elements.map(renderValue).join('') | |
visitMixinDefinition: (node, options) -> | |
options.visitDeeper = false | |
name = node.name.slice(1) | |
name = mixinMap[name] or name | |
@p "#{name}(#{node.params.map(renderMixinParam).join(', ')})" | |
for rule in node.rules | |
renderTree(rule, @increaseIndent()) | |
if node.params.length == 0 or node.params.every((p) -> p.value?) | |
@p ".#{name}" | |
@p "#{name}()", @increaseIndent() | |
visitMixinCall: (node, options) -> | |
options.visitDeeper = false | |
if node.selector.elements.length == 2 and node.selector.elements[0].value[0] == '#' | |
namespace = node.selector.elements[0].value.slice(1) | |
node.selector.elements[0] = node.selector.elements[1] | |
delete node.selector.elements[1] | |
node.selector.elements[0].value = "#{namespace}-#{node.selector.elements[0].value.slice(1)}" | |
name = renderValue(node.selector).slice(1) | |
name = mixinMap[name] or name | |
if node.arguments.length > 0 | |
v = "#{renderValue(node.selector).slice(1)}" | |
v += "(#{node.arguments.map(renderMixinArg).join(', ')})" | |
else | |
# TODO: what about mixins with defaults? we need to generate classes | |
# for them | |
v = "@extend .#{renderValue(node.selector).slice(1)}" | |
@p v | |
visitImport: (node, options) -> | |
options.visitDeeper = false | |
@p "@import #{renderValue(node.path).replace(/\.less/, '.styl')}" | |
visitDirective: (node, options) -> | |
@visitRuleset(node.ruleset, options, node.name) | |
parser.parse str, (err, node) -> | |
process.exit(1) if err | |
renderPrelude() | |
renderTree node |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment