-
Notifications
You must be signed in to change notification settings - Fork 8
/
grid-notation.coffee
935 lines (807 loc) · 28.8 KB
/
grid-notation.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
class GridNotation
constructor: (args = {}) ->
@unit = new Unit()
@cmd = new Command()
# Convert a grid notation string into an array of guides.
#
# string - string to parse
# info - information about the document
#
# Returns an Array.
parse: (string = "", info = {}) ->
@unit.resolution = info.resolution if info.resolution
@cmd.unit = @unit
guides = []
tested = @validate(@objectify(string))
return null if tested.errors.length > 0
gn = tested.obj
for key, variable of gn.variables
gn.variables[key] = @expandCommands variable, variables: gn.variables
for grid in gn.grids
guideOrientation = grid.params.orientation
wholePixels = grid.params.calculation is 'p'
fill = find(grid.commands, (el) -> el.isFill)[0] || null
originalWidth = if guideOrientation == 'h' then info.height else info.width
measuredWidth = if guideOrientation == 'h' then info.height else info.width
measuredWidth = grid.params.width.unit.base if grid.params.width?.unit?.base
offset = if guideOrientation == 'h' then info.offsetY else info.offsetX
stretchDivisions = 0
adjustRemainder = 0
wildcardArea = measuredWidth
# Expand any fills or variables
grid.commands = @expandCommands grid.commands, gn.variables
wildcards = find grid.commands, (el) -> el.isWildcard
# If a width was specified, position it.
if grid.params.width?.unit?.base
adjustRemainder = originalWidth - grid.params.width?.unit.base
# Set value of percent commands
percents = find grid.commands, (el) -> el.isPercent
for k,v of gn.variables
percents = percents.concat find v, (el) -> el.isPercent
for command in percents
percentValue = measuredWidth*(command.unit.value/100)
percentValue = Math.floor(percentValue) if wholePixels
command.unit = @unit.parse("#{ percentValue }px")
# Measure explicit commands
explicit = find grid.commands, (el) -> el.isExplicit and !el.isFill
explicitSum = 0
explicitSum += command.unit.base for command in explicit
wildcardArea -= explicitSum
wildcardArea = 0 if wildcardArea < 0
# Calculate fills
if fill
length = lengthOf(fill, gn.variables)
fillIterations = 0
fillIterations = Math.floor(wildcardArea/length) if length > 0
fillCollection = []
fillWidth = 0
for i in [1..fillIterations]
if fill.isVariable
fillCollection = fillCollection.concat gn.variables[fill.id]
fillWidth += lengthOf(fill, gn.variables)
else
newCommand = @cmd.parse(@cmd.toSimpleString(fill))
fillCollection.push newCommand
fillWidth += newCommand.unit.base
wildcardArea -= fillWidth
newCommands = []
for command, i in grid.commands
if command.isFill
newCommands = newCommands.concat fillCollection
else
newCommands.push command
grid.commands = [].concat newCommands
# Measure explicit commands
explicit = find grid.commands, (el) -> el.isExplicit and !el.isFill
explicitSum = 0
explicitSum += command.unit.base for command in explicit
# If the width wasn't specified, subtract the offsets from the boundaries.
if !grid.params.width?.unit?.base?
adjustRemainder = originalWidth - explicitSum if wildcards.length == 0
wildcardArea -= grid.params.firstOffset?.unit?.base || 0
wildcardArea -= grid.params.lastOffset?.unit?.base || 0
if adjustRemainder > 0
adjustRemainder -= grid.params.firstOffset?.unit?.base || 0
adjustRemainder -= grid.params.lastOffset?.unit?.base || 0
# Calculate wild offsets
stretchDivisions++ if grid.params.firstOffset?.isWildcard
stretchDivisions++ if grid.params.lastOffset?.isWildcard
adjust = adjustRemainder/stretchDivisions
if grid.params.firstOffset?.isWildcard
adjust = Math.ceil(adjust) if wholePixels
grid.params.firstOffset = @cmd.parse("#{ adjust }px")
if grid.params.lastOffset?.isWildcard
adjust = Math.floor(adjust) if wholePixels
grid.params.lastOffset = @cmd.parse("#{ adjust }px")
# Adjust the first offset.
offset += grid.params.firstOffset?.unit.base || 0
# Set the width of any wildcards
if wildcardArea? and wildcards
wildcardWidth = wildcardArea/wildcards.length
if wholePixels
wildcardWidth = Math.floor wildcardWidth
remainderPixels = wildcardArea % wildcards.length
for command in wildcards
command.isWildcard = false
command.isExplicit = true
command.isFill = true
command.multiplier = 1
command.isPercent = false
command.unit = @unit.parse("#{ wildcardWidth }px")
# Adjust for pixel specific grids
if remainderPixels
remainderOffset = 0
if grid.params.remainder == 'c'
remainderOffset = Math.floor (wildcards.length - remainderPixels)/2
if grid.params.remainder == 'l'
remainderOffset = wildcards.length - remainderPixels
for command, i in wildcards
if i >= remainderOffset && i < remainderOffset + remainderPixels
command.unit = @unit.parse("#{ wildcardWidth+1 }px")
# Figure out where the grid starts
insertMarker = offset
# Remove dupe guides
newCommands = []
for command, i in grid.commands
if !command.isGuide or (command.isGuide and !grid.commands[i-1]?.isGuide)
newCommands.push command
grid.commands = [].concat newCommands
for command in grid.commands
if command.isGuide
guides.push
location: insertMarker
orientation: guideOrientation
else
insertMarker += command.unit.base
guides
# Format a grid notation string according to spec.
#
# string - string to format
#
# Returns a String.
clean: (string = "") =>
gn = @validate(@objectify(string)).obj
string = ""
for key, variable of gn.variables
string += "#{ key } = #{ @stringifyCommands variable }\n"
string += "\n" if gn.variables.length > 0
for grid in gn.grids
line = ""
line += @stringifyCommands grid.commands
line += " #{ @stringifyParams grid.params }"
string += "#{ trim line }\n"
trim string.replace(/\n\n\n+/g, "\n")
# Convert an object of grid inputs into a grid notation string.
#
# grid - Object to stringify.
# count - int, string int: number of columns/rows
# width - unit pair string: width of column/row
# gutter - unit pair string: width of gutter
# firstMargin - unit pair string: left/top margin
# lastMargin - unit pair string: right/bottom margin
# columnMidpoint - true, false: split the columns/rows in two
# gutterMidpoint - true, false: split the gutters in two
# orientation - h, v
# position - first, center, last
# remainder - first, center, last
#
# Returns a String.
stringify: (data) =>
data ||= {}
data.count = parseInt data.count
data.width ||= ''
data.gutter ||= ''
data.firstMargin ||= ''
data.lastMargin ||= ''
data.columnMidpoint ||= false
data.gutterMidpoint ||= false
data.orientation ||= 'v'
data.position ||= 'f'
data.remainder ||= 'l'
data.calculation ||= ''
firstMargString = ''
varString = ''
gridString = ''
lastMargString = ''
optionsString = ''
# Set up the margins, if they exist
firstMargString = '|' + data.firstMargin.replace(/,|\s+/g,' ').split(' ').join('|') + '|' if data.firstMargin
lastMargString = '|' + data.lastMargin.replace(/,|\s+/g,' ').split(' ').reverse().join('|') + '|' if data.lastMargin
# Set up the columns and gutters variables, if they exist
if data.count or data.width
column = if data.width then data.width else '~'
if data.columnMidpoint
left = right = '~'
if data.width
unit = @unit.parse(data.width)
left = "#{if data.calculation is 'p' and unit.type is 'px' then Math.floor(unit.value/2) else unit.value/2}#{ unit.type }"
right = "#{if data.calculation is 'p' and unit.type is 'px' then Math.ceil(unit.value/2) else unit.value/2}#{ unit.type }"
column = "#{ left }|#{ right }"
varString += "$#{ data.orientation } = |#{ column }|\n"
if data.gutter and data.count != 1
gutter = if data.gutter then data.gutter else '~'
if data.gutterMidpoint
left = right = '~'
if data.gutter
unit = @unit.parse(data.gutter)
left = "#{if data.calculation is 'p' and unit.type is 'px' then Math.floor(unit.value/2) else unit.value/2}#{ unit.type }"
right = "#{if data.calculation is 'p' and unit.type is 'px' then Math.ceil(unit.value/2) else unit.value/2}#{ unit.type }"
gutter = "#{ left }|#{ right }"
varString = "$#{ data.orientation } = |#{ column }|#{ gutter }|\n"
varString += "$#{ data.orientation }C = |#{ column }|\n"
# Set up the grid string
if data.count or data.width
gridString += "|$#{ data.orientation }"
gridString += "*" if data.count != 1
gridString += data.count - 1 if data.count > 1 and data.gutter
gridString += data.count if data.count > 1 and !data.gutter
gridString += "|"
gridString += "|$#{ data.orientation }#{ if data.gutter then 'C' else '' }|" if data.gutter and data.count != 1
if (!data.count and !data.width) and data.firstMargin
gridString += "|"
if (!data.count and !data.width) and (data.firstMargin or data.lastMargin)
gridString += "~"
if (!data.count and !data.width) and data.lastMargin
gridString += "|"
if data.firstMargin or data.lastMargin or data.count or data.width
# Set up the options
optionsString += " ( "
optionsString += data.orientation
optionsString += data.remainder
optionsString += "p" if data.calculation == "p"
optionsString += ", "
optionsString += "~" if data.position is "l" or data.position is "c"
optionsString += "|"
optionsString += "~" if data.position is "f" or data.position is "c"
optionsString += " )"
# Bring it all together
@pipeCleaner "#{ varString }#{ firstMargString }#{ gridString }#{ lastMargString }#{ optionsString }".replace(/\|+/g, "|")
# Return an array of errors that exist in a grid notation string.
#
# string - string to test
#
# Returns an Array.
test: (string) =>
@validate(@objectify(string)).errors
# Create an object of grid data from a grid notation string
#
# string - string to parse
#
# Returns an object
objectify: (string = "") =>
lines = string.split /\n/g
string = ""
variables = {}
grids = []
for line in lines
if /^\$.*?\s?=.*$/i.test line
variable = @parseVariable line
variables[variable.id] = variable.commands
else if /^\s*#/i.test line
# ignored line
else
grid = @parseGrid line
grids.push grid if grid.commands.length > 0
variables: variables
grids: grids
# Process a grid notation object looking for errors. If any exist, mark them
# and return the results.
#
# obj - grid notation object
#
# Returns an Object.
validate: (obj) =>
variablesWithWildcards = {}
errors = []
error(2, errors) if obj.grids.length <= 0
for key, commands of obj.variables
for command in commands
error(command.errors, errors) if command.errors.length > 0
id = command.id
variable = obj.variables[id] if id
# If an undefined variable is called, we can't do anything with it.
error(6, errors, command) if id and !variable
# Fills are only meant to be used once, in one place. Including a fill
# in a variable likely means it will be used in multiple places. In
# theory this *could* be used once, but for now, let's just invalidate.
error(5, errors, command) if command.isFill
variablesWithWildcards[key] = true if command.isWildcard
for key, grid of obj.grids
fills = 0
# Determine if the adjustments are valid
first = grid.params.firstOffset
width = grid.params.width
last = grid.params.lastOffset
error(1, errors) if first and first.errors.length > 0
error(1, errors) if width and width.errors.length > 0
error(1, errors) if last and last.errors.length > 0
for command in grid.commands
error(command.errors, errors) if command.errors.length > 0
id = command.id
variable = obj.variables[id] if id
# If an undefined variable is called, we can't do anything with it.
error(6, errors, command) if id and !variable
# Since wildcards don't have an inherent value, it's impossible to
# calculate a fill variable containing one.
varHasWildcard = find(variable, (el) -> el.isWildcard).length > 0
# Fill variables cannot contain wildcards
error(3, errors, command) if command.isFill and varHasWildcard
fills++ if command.isFill
varHasFill = find(variable, (el) -> el.isFill).length > 0
# count as a fill if it's a variable that contains a fill
if id and variable and varHasFill
fills++
# Fills can only be used once.
error(4, errors, command) if fills > 1
error(5, errors, command) if id and variable and varHasFill
errors: errors.sort()
obj: obj
# Convert a string of command and guide commands into an object.
#
# Returns a command object
parseCommands: (string = "") ->
string = @pipeCleaner string
commands = []
return commands if string == ""
tokens = string.replace(/^\s+|\s+$/g, '').replace(/\s\s+/g, ' ').split(/\s/)
commands.push(@cmd.parse(token)) for token in tokens
commands
# Take an array of commands and apply any multiples
#
# array - array of commands
# variables - variables for expanding
#
# Returns an Array
expandCommands: (commands = [], variables = {}) ->
commands = @parseCommands commands if typeof commands is "string"
reparse = true
varWidths = {}
while reparse is true
reparse = false
# Expand any multipliers
newCommands = []
for command in commands
if command.isFill
newCommands.push command
else
loops = command.multiplier || 1
for i in [0...loops] by 1
newCommands.push @cmd.parse(@cmd.toSimpleString(command))
commands = [].concat newCommands
# Apply any variables
newCommands = []
for command, i in commands
if command.isVariable and variables and variables[command.id] and !command.isFill
reparse = true
newCommands = newCommands.concat(variables[command.id])
else
newCommands.push command
commands = [].concat newCommands
commands
# Look into a string to see if it contains commands
#
# string - string to test
#
# Returns a Boolean
isCommands: (string = "") =>
return false if string is ""
return true if string.indexOf("|") >= 0 # it has pipes
commands = @parseCommands string
return true if commands.length > 1 # it has multiple commands
return true if commands[0].errors.length is 0 # it has a valid first command
false
# Convert a grid string into an object.
#
# string - string to parse
#
# Returns an Object.
parseGrid: (string = "") =>
regex = /\((.*?)\)/i
params = regex.exec(string) || []
string = trim string.replace regex, ''
commands = @parseCommands string
commands: commands
wildcards: find commands, (el) -> el.isWildcard
params: @parseParams params[1] || ''
# Deterimine a grid's paramaters
#
# string - string to be parsed
#
# Returns an Object.
parseParams: (string = "") =>
bits = string.replace(/[\s\(\)]/g,'').split ','
obj =
orientation: "h"
remainder: "l"
calculation: ""
if bits.length > 1
obj[k] = v for k,v of @parseOptions bits[0]
obj[k] = v for k,v of @parseAdjustments(bits[1] || "")
return obj
else if bits.length is 1
if @isCommands bits[0]
obj[k] = v for k,v of @parseAdjustments(bits[0] || "")
else
obj[k] = v for k,v of @parseOptions bits[0]
obj
# Determine a grid's options
#
# string - string to be parse
#
# Returns an Object.
parseOptions: (string = "") ->
options = string.split ''
obj = {}
for option in options
switch option.toLowerCase()
when "h", "v"
obj.orientation = option
when "f", "c", "l"
obj.remainder = option
when "p"
obj.calculation = option
obj
# Determine a grid's position
#
# string - string to be parsed
#
# Returns an Object or null if invalid.
parseAdjustments: (string = "") ->
adj =
firstOffset: null
width: null
lastOffset: null
return adj if string is ""
bits = @expandCommands(string.replace(/\s/,'')).splice(0,5)
end = bits.length-1
adj.lastOffset = bits[end] if bits.length > 1 and !bits[end].isGuide
adj.firstOffset = bits[0] if !bits[0].isGuide
for el, i in bits
if bits[i-1]?.isGuide and bits[i+1]?.isGuide
adj.width = el if !el.isGuide
adj
# Determine a variable's id, and gaps
#
# string - variable string to be parsed
#
# Return an object
parseVariable: (string) =>
bits = /^\$([^=\s]+)?\s?=\s?(.*)$/i.exec(string)
return null if !bits[2]?
id: if bits[1] then "$#{ bits[1] }" else "$"
commands: @parseCommands bits[2]
# Clean up the formatting of pipes in a command string
#
# string - string to be cleaned
#
# Returns a String.
pipeCleaner: (string = "") ->
string
.replace(/[^\S\n]*\|[^\S\n]*/g, '|') # Normalize spaces
.replace(/\|+/g, ' | ') # Duplicate pipes
.replace(/^\s+|\s+$/gm, '') # Leading and trailing whitespace
# Convert a command array into a grid notation spec compliant string.
#
# commands - command array
#
# Returns a String.
stringifyCommands: (commands) =>
string = ""
string += " " + @cmd.stringify(command) for command in commands
@pipeCleaner string
# Convert a grid's params to a guiden notation spec compliant string.
#
# params - grid params object
#
# Returns a String.
stringifyParams: (params) =>
string = ""
string += "#{ params.orientation || '' }"
string += "#{ params.remainder || '' }"
string += "#{ params.calculation || '' }"
if params.firstOffset or params.width or params.lastOffset
string += ", " if string.length > 0
string += @cmd.stringify(params.firstOffset) if params.firstOffset
string += "|" if params.firstOffset or params.width
string += "#{ @cmd.stringify(params.width) }" if params.width
string += "|" if params.lastOffset or params.width
string += @cmd.stringify(params.lastOffset) if params.lastOffset
if string then "( #{ @pipeCleaner(string) } )" else ''
#
# A command tells the guide parser to move ahead by the specified distance, or
# to add a guide.
#
class Command
variableRegexp: /^\$([^\*]+)?(\*(\d+)?)?$/i
explicitRegexp: /^(([-0-9\.]+)?[a-z%]+[0-9\.]*)(\*(\d+)?)?$/i
pointPicaRegexp: /^[-0-9\.]+p[0-9\.]*$/
wildcardRegexp: /^~(\*(\d*))?$/i
constructor: (args = {}) ->
@unit = new Unit()
# Test if a command is a guide
#
# command - command to test
#
# Returns a Boolean
isGuide: (command = "") ->
if typeof command is "string"
command.replace(/\s/g, '') == "|"
else
command.isGuide || false
# Test if a string is a variable
#
# string - command string to test
#
# Returns a Boolean
isVariable: (command = "") =>
if typeof command is "string"
@variableRegexp.test command.replace /\s/g, ''
else
command.isVariable || false
# Test if a command is an arbitray command (unit pair)
#
# string - command string to test
#
# Returns a Boolean
isExplicit: (command = "") =>
if typeof command is "string"
command = command.replace /\s/g, ''
return false if !@explicitRegexp.test(command)
return false if @unit.parse(command) == null
true
else
command.isExplicit || false
# Test if a command is a wildcard
#
# string - command string to test
#
# Returns a Boolean
isWildcard: (command = "") =>
if typeof command is "string"
@wildcardRegexp.test command.replace /\s/g, ''
else
command.isWildcard || false
# Test if a command is a percent
#
# string - command string to test
#
# Returns a Boolean
isPercent: (command = "") ->
if typeof command is "string"
unit = @unit.parse(command.replace /\s/g, '')
unit? and unit.type == '%'
else
command.isPercent || false
# Test if a command does not have a multiple defined, and therefor should be
# repeated to fill the given area
#
# string - command string to test
#
# Returns a Boolean
isFill: (string = "") ->
if @isVariable string
bits = @variableRegexp.exec string
return bits[2] && !bits[3] || false
else if @isExplicit string
bits = @explicitRegexp.exec string
return bits[3] && !bits[4] || false
else if @isWildcard string
bits = @wildcardRegexp.exec string
return bits[1] && !bits[2] || false
else
false
# Parse a command and return the number of multiples
#
# string - wildcard string to parse
#
# Returns an integer
count: (string = "") ->
string = string.replace /\s/g, ''
if @isVariable string
parseInt(@variableRegexp.exec(string)[3]) || 1
else if @isExplicit string
parseInt(@explicitRegexp.exec(string)[4]) || 1
else if @isWildcard string
parseInt(@wildcardRegexp.exec(string)[2]) || 1
else
null
# Parse a command into its constituent parts
#
# string - command string to parse
#
# Returns an object
parse: (string = "") ->
string = string.replace /\s/g, ''
if @isGuide string
errors: []
isGuide: true
else if @isVariable string
bits = @variableRegexp.exec string
errors: []
isVariable: true
isFill: @isFill string
id: if bits[1] then "$#{ bits[1] }" else "$"
multiplier: @count string
else if @isExplicit string
errors: []
isExplicit: true
isPercent: @isPercent string
isFill: @isFill string
unit: @unit.parse(string)
multiplier: @count string
else if @isWildcard string
errors: if @isFill(string) then [3] else []
isWildcard: true
isFill: @isFill string
multiplier: @count string
else
errors: [1]
string: string
# Output a command as a string. If it is unrecognized, format it properly.
#
# command - command to be converted to a string
#
# Returns an Integer.
stringify: (command = "") ->
return command if typeof command is "string"
string = ""
if command.isGuide
string += "|"
else if command.isVariable
string += command.id
else if command.isExplicit
string += @unit.stringify(command.unit)
else if command.isWildcard
string += "~"
else
return "" if command.string is ""
string += command.string
if command.isVariable or command.isExplicit or command.isWildcard
string += '*' if command.isFill or command.multiplier > 1
string += command.multiplier if command.multiplier > 1
if command.errors.length is 0 then string else "{#{ string } [#{ command.errors.join(',') }]}"
# Create a command string without a multiplier
#
# command - command to stringify
#
# Returns a String.
toSimpleString: (command = "") ->
return command.replace(/\*.*/gi, "") if typeof command is "string"
@stringify(command).replace(/[\{\}]|\*.*|\[\d*\]/gi, "")
#
# Unit is a utility for parsing and validating unit strings
#
class Unit
resolution: 72
constructor: (args = {}) ->
# Parse a string and change it to a unit object
#
# string - unit string to be parsed
#
# Returns an object or null if invalid
parse: (string = "") =>
string = string.replace /\s/g, ''
bits = string.match(/[-0-9\.]+p[0-9\.]*(?![a-z])/i)
bits = @normalizePoints(bits[0]) if bits
bits = string.match(/([-0-9\.]+)([a-z%]+)?/i) if !bits
return null if !string or string == "" or !bits?
return null if bits[2] and !@preferredName(bits[2])
# Integer
if bits[1] and !bits[2]
zeroAdjustedValue = bits[1].replace(/^\./, '0.')
value = parseFloat bits[1]
return if value.toString() == zeroAdjustedValue then value else null
# Unit pair
string: string
value: parseFloat bits[1]
type: @preferredName bits[2]
base: @asBaseUnit value: parseFloat(bits[1]), type: @preferredName(bits[2])
normalizePoints: (string) ->
bits = /(-)?([0-9\.]+)p([0-9\.]*)$/i.exec(string)
negative = bits[1]?
picas = parseFloat(bits[2] || '0')
points = parseFloat(bits[3] || '0')
total = (points + (picas * 6)) * (if negative then -1 else 1)
[
string,
total,
'point'
]
pointsToString: (n) ->
"#{ Math.floor(n / 6)}p#{ if n % 6 then n % 6 else '' }"
# Parse a string and change it to a friendly unit
#
# string - string to be parsed
#
# Returns a string or null, if invalid
preferredName: (string) ->
switch string
when 'centimeter', 'centimeters', 'centimetre', 'centimetres', 'cm'
'cm'
when 'inch', 'inches', 'in'
'in'
when 'millimeter', 'millimeters', 'millimetre', 'millimetres', 'mm'
'mm'
when 'pixel', 'pixels', 'px'
'px'
when 'point', 'points', 'pts', 'pt', 'p'
'points'
when 'pica', 'picas'
'picas'
when 'percent', 'pct', '%'
'%'
else
null
# Convert the given value of type to the base unit of the application.
# This accounts for reslution, but the resolution must be manually changed.
# The result is pixels/points.
#
# unit - unit object
# resolution - dots per inch
#
# Returns a number
asBaseUnit: (unit) ->
return null unless unit? and unit.value? and unit.type?
# convert to inches
switch unit.type
when 'cm' then unit.value = unit.value / 2.54
when 'in' then unit.value = unit.value / 1
when 'mm' then unit.value = unit.value / 25.4
when 'px' then unit.value = unit.value / @resolution
when 'points' then unit.value = unit.value / 72
when 'picas' then unit.value = unit.value / 12
else
return null
# convert to base units
unit.value * @resolution
# Convert a unit object to a string or format a unit string to conform to the
# unit string standard
#
# unit = string or object
#
# Returns a string
stringify: (unit = "") =>
return null if unit == ""
return @stringify(@parse(unit)) if typeof unit == "string"
if unit.type is 'points'
@pointsToString(unit.value)
else if unit.type is 'picas'
@pointsToString(unit.value * 6)
else
"#{ unit.value }#{ unit.type }"
# Remove leading and trailing whitespace
#
# string - string to be trimmed
#
# Returns a String.
trim = (string) -> string.replace(/^\s+|\s+$/g, '')
# Find all items in an array that match the iterator
#
# arr - array
# iterator - condition to match
#
# Returns a array.
find = (arr, iterator) ->
return [] unless arr and iterator
matches = []
(matches.push el if iterator(el) is true) for el, i in arr
matches
# Get the total length of the given command.
#
# command - command to be measured
# variables - variables from the grid notation
#
# Returns a Number.
lengthOf = (command, variables) ->
return command.unit.value * command.multiplier unless command.isVariable
return 0 if !variables[command.id]
sum = 0
for command in variables[command.id]
sum += command.unit?.base || 0
sum
# Assign an error code to a command and a master list
#
# code - error code to assign
# master - the master error array
# command - the command that caused the error
#
# Returns nothing.
error = (codes, master, command) ->
codes = [codes] if typeof codes is "number"
for code in codes
exists = find(master, ((e, i) -> true if e is code)).length > 0
master.push code if !exists
return unless command
command.errors ||= []
command.isValid = false
exists = find(command.errors, ((e, i) -> true if e is code)).length > 0
command.errors.push code if !exists
if (typeof module != 'undefined' && typeof module.exports != 'undefined')
module.exports =
notation: new GridNotation()
unit: new Unit()
command: new Command()
else
window.GridNotation = new GridNotation()
window.Unit = new Unit()
window.Command = new Command()