WWW

CSS Factoring

Purpose

To factor out common parts in CSS files.

Program executable and source can be downloaded from links on the page CSS Factoring Manual

Rationale

Many style sheets use identical specifications in the rules for several elements.  For example, you may want the same border colour for several types of table cells and also for horizontal rulers.  Or you want the left margin of paragraphs, headings, lists and divisions to be the same.

In your style sheet you may have:

hr { background-color: rgb(30%,0%,0%); }

td { border: solid 0.1em rgb(30%,0%,0%); }

h3 { border-top: solid 0.1em rgb(30%,0%,0%); }

But these rules may be far apart in the code.

CSS, for good reasons, does not allow factoring out such common parts.  If you want to change the colour of those three types of line in the example above, you must do so at every place where it is used and hope not to miss one.  This is a nuisance if you want to experiment and/or if you maintain several sites with similar appearance, or if you want subsets of your site to look the same in layout but to differ in their colour schemes.

It would be more effective for style sheet maintenance if you could write:

define BasicLineColour rgb(30%,0%,0%)
...
hr { background-color: BasicLineColour; }
...
td { border: solid 0.1em BasicLineColour; }
...
h3 { border-top: solid 0.1em BasicLineColour; }

Then you can change the colour in one place.

The CSS Factoring program lets you do just that:  attach values to identifiers so you can use the identifiers in all places where that value occurs.

As mentioned, colours are not the only specifications that occur over and over again in the same style sheet.  Margins are another case.

Many lengths such as margins have values related to the font size of the text.  Any specification of a length may depend on another value.  Especially lengths expressed in em size are difficult to deal with:  for good reasons an em size is relative to the font size of the element it specifies (not the surrounding element).  As an example, suppose you want all left margins of div elements to display the same on the screen, say 3em, but the font size of each div is different, say for one it is 1em and for another it is 1.5em.  If you set the left margin to 3em for both, then the left margin of the div with font size 1.5em will actually be 1.5 times larger on the screen than that of the div with font size 1em.  Therefore, to make them look the same, the specifications should be:

div1 { font-size: 1em; margin-left: 3em; }

div2 { font-size: 1.5em; margin-left: 2em; }

This cannot be factored out simply.

You would need either two identifiers, as in:

define Div1LeftMargin 3em

define Div2LeftMargin 2em

...

div1 { font-size: 1em; margin-left: Div1LeftMargin; }

div2 { font-size: 1.5em; margin-left: Div2LeftMargin; }

and that is not very helpful;  or you could have one identifier and allow expressions:

define MainLeftMargin 3em

...

div1 { font-size: 1em; margin-left: MainLeftMargin; }

div2 { font-size: 1.5em; margin-left: MainLeftMargin/1.5; }

But MainLeftMargin is not just a numeric value, it has a unit.  That leads to two different problems.

Brainstorming

If you have grasped the usefulness of CSS Factoring and do not want to read the rambling ideas-list below, but just want to use the program, then jump to the CSS Factoring Manual.  The restof this page is devoted to the inner workings and program code.

Let's do some brainstorming, just to get a clear idea of what we need to make factoring work.

First, to work with string replacements only, we would have to append the em unit somewhere, like:

define MainLeftMargin 3

...

div1 { font-size: 1em; margin-left: MainLeftMarginem; }

div2 { font-size: 1.5em; margin-left: MainLeftMargin/1.5em; }

This looks terrible.

The second problem is how to find the expressions.

We could use delimiters around them so we can find them easily.  It does not look as nice, but the problem of the unit remains:

define MainLeftMargin 3em

...

div1 { font-size: 1em; margin-left: MainLeftMargin; }

div2 { font-size: 1.5em; margin-left: {{MainLeftMargin/1.5}}; }

The problem of using the string 3em in the expression remains.  Expressions return numerical values, so we could define the numerical value of a definition as the value string with all non-numeric characters removed.  Consider:

define MainLeftMargin 4.5em

define BasicLineColour rgb(30%,0%,0%)

The value of MainLeftMargin is "4.5em" (a string) and its numerical value is "4.5" (a number);  the value of BasicLineColour is "rgb(30%,0%,0%)" and its numerical value is "3000" (a number, though meaningless).  We can then evaluate

define MainLeftMargin 4.5em

define MainRightMargin MainLeftMargin/2

define BasicLineColour rgb(30%,0%,0%)

define Senseless 2+BasicLineColour

by using the numeric values and so get MainRightMargin to be 2.25 and Senseless to be 3002.  Unfortunately now we have lost the em unit needed for MainRightMargin!

The way out is to acknowledge that units exist.  They are placed at the end of numeric values and are in the set em px % mm cm pt and a few others, the most common being em, %, px.

Another useful observation is that all definitions involving expressions must at some prior point be dependent on definitions that are not expressions.  For example, in the above MainLeftMargin is independent from everything else but may be used in subsequent expressions.  We will call such definitions parameters and use that explicitly in the syntax:

parameter MainLeftMargin 3em

define MainRightMargin MainLeftMargin/2

So now we can require that a parameter is a numeric value immediately followed by a unit.  Then a parameter has a name, a value and a unit.  The units can then be combined separately from the values when evaluating an expression.  Define statements define a name, a value, a unit and in addition a string value.  In processing the define statement for MainRightMargin we will expect no units in the expression.  We will use MainLeftMargin's value (3), divide it by 2 to yield 1.5 and then concatenate the unit (em) to get 1.5em.  MainRightMargin has a value of 1.5 a unit of em and a string value of 1.5em.

The sequence:

parameter MainLeftMargin 3em

...

define Div2LeftMargin MainLeftMargin/1.5

...

div1 { font-size: 1em; margin-left: MainLeftMargin; }

div2 { font-size: 1.5em; margin-left: Div2LeftMargin; }

is now easy to deal with, but we can also write:

parameter MainLeftMargin 3em

...

div1 { font-size: 1em; margin-left: MainLeftMargin; }

div2 { font-size: 1.5em; margin-left: {{MainLeftMargin/1.5}}; }

There is still a restriction on our imagination.  We might have a reason to write

parameter BoxWidth 3em*40

because the 40 may be something obvious in the same way as the 4 in .  But for now we will have to write this as:

parameter SomeThing 3em

define BoxWidth SomeThing*40

It still does not work:  what if we have

define H1Borders border-top: solid 0.15em rgb(67%,67%,33%); border-bottom: none

since that is not an expression but an unquoted string value.

A what if we later decide the style sheet is more maintainable if we write:

parameter StandardBorderWidth 0.15em

...

define H1Borders border-top: solid StandardBorderWidth rgb(67%,67%,33%); border-bottom: none

or worse:

parameter StandardBorderWidth 0.15em

...

define H1Borders border-top: solid StandardBorderWidth+2 rgb(67%,67%,33%); border-bottom: none

We could introduce more "reserved words":

parameter StandardBorderWidth 0.15em

parameter BaseRedIntensity 10%

compute RedIntensity 2*BaseRedIntensity

define H1Borders border-top: solid {{StandardBorderWidth+2}} rgb(RedIntensity,67%,33%); border-bottom: none

StandardBorderWidth and RedIntensity have a value, unit and stringvalue.  The define has a string value only.  But it is over-complicated.

I reflected some more, all with the intention to avoid a full expression parsing algorithm, but in the end I had to give up.  Fortunately, we do not have to write an expression evaluation algorithm:  LiveCode has one built-in, and we can use the "try" statement to catch any errors.

Splitting into multiple files

I have some very long style sheets and found it useful to split them over several files, one for each group of style specifications, such as tables, headings, lists and so on.  Using separate files makes maintenance of style sheets over several sites and several media easier:  not all groups are used in all sites.

Merging the individual files back into a single style sheet can be done by "include" statements and these are easy to process.

@media

I discovered that at least for some browsers it is not possible to have more than one @media rule set for a given media.  For example, suppose you give rules for headings (<h1>,<h2>,…) and follow them with an @media print { … } section in which you specify what they should look like on the printed page.  Then further on in your style sheet you give rules for lists.  You cannot now start another section @media print { … } to specify what to do with lists in print.  In other words, you have to put all the print rules together.  This may not apply to all browsers.

It may be more convenient to keep all rules for a certain set of html elements together, i.e. keep the style sheet sorted by element kinds (headings, tables, lists, …) rather than sorting it by media type (screen, print, …).  It is not a requirement, but I certainly find it more productive to do it that way.

To deal with the problem the program sorts all @media rule sets per media type and puts them at the end of the sheet, after all rules that are not media dependent.  This can be done fairly easily with arrays.  Unfortunately it means scanning for the } that ends the @media rules.  Not only is this time consuming, but it is not reliable unless we construct a real parser that also finds errors in the rules instead of just running after a matching }. In this version I have not constructed a full language parser, instead the program assumes that all parentheses match properly.  As an author of CSS style sheets you are warned that there may be errors in your code that will easily slip through the factoring program.

Sorting the @media rule sets presented one other problem:  it is possible to specify a single rule for several media.  In that case the list of media can be given as a comma-separated list.  The program however will duplicate such rule sets in the corresponding media set, i.e. in the processed file there will be only one media specified per @media rule set. This is not a restriction.

Because this rule sorting does not apply universally and will probably be fixed for all browsers in the near future, the application will only sort the media rules if the option-key is held down.

The “Language”

Files

All files involved are text files, with extension scss (source CSS) and containing CSS specifications as well as our language statements.

There are two statements and one in-line construct:

Each statement has to occur on a line by itself;  in-line expressions can occur anywhere.

Include

The syntax is:

include <relative-file-path>

where the relative file path is a path from the file with the include statement to the included file.  Files making up a style sheet will usually sit in the same folder.

Define

The syntax is:

define <identifier> <expression>

where the expression can have several forms:

Here are some examples:

define MainLeftMargin 3.5em

define H1Borders border-top: solid 0.15em rgb(67%,67%,33%); border-bottom: none

define QuoteLeftMargin MainLeftMargin/2.3 + 5.5

define BeforeItem UsualStuff & "yakity"

The first defines MainLeftMargin to be a parameter with value 3.5 and unit em.

The second defines the string "H1Borders border-top: solid 0.15em rgb(67%,67%,33%); border-bottom: none"

The third defines a value of 3.5/2.3+5.5 = 7.022 with a unit of em (which is inherited from the identifier MainLeftMargin).

Finally the fourth defines a string by concatenating the string value of UsualStuff (not given here) with the string "yakity".

The last two are examples of expressions in definitions, one numeric the other string.

In-line Expressions

The syntax is:

... {{ <expression> }} ...

whereby the expression may contain identifiers from define statements, and must evaluate properly when all the identifiers have been replaced by their values.

Note

It is possible to define an identifier as another identifier, i.e. this does work:

define X 4em

define Y X

define Z X

so that Y and Z will be defined with value 4 and unit em.  Although it looks superfluous, it may be useful to give such definitions e.g. in the case where different style sheets (perhaps for different sites) are constructed from a set of shared scss files, whereby Y and Z have different values from X in most sheets but not in all.

Parsing the Language

Include Statements

To handle a file, we first replace all the include statements with the text from the files they refer to.  We allow no nesting of include files for now.

This gives a text which contains only define statements and CSS syntax;  with possibly in-line expressions.

Define Statements

The define statement syntax requires a lexical scanner that recognises these lexical tokens:

Anything else is "undefined".  Obviously anything containing an undefined token is a malformed expression.

The lexical scanner stops at the end of the line and returns the tokens it has found in two variables, one contains the tokens one per line, the other the values one per line.

It can look at the list of tokens it found and attribute a kind to the expression:

Thus for 3.5em the lexical scanner returns:

TokensValues
number3.5
unitem

But for -12px it returns:

TokensValues
operator-
number12
unitpx

The above two cases being the ones that lead to a parameter definition.  Unquoted strings are stored as if they were quoted, it is just easier not to have to use quotes around, say, colour values.

The cases that look like expressions to the lexical scanner can still be wrong, but catching that error is the task of the LiveCode expression evaluator (as used in a "do" statement).

In-line Expressions

That leaves in-line expressions.  The program finds them by looking for the {{ and }} parentheses, and assuming that between them sits a valid expression.  It replaces all identifiers by their values and keeps the unit of the last one found as the unit to be used (if it exists).  It then evaluates the expression and if it succeeds it replaces the parentheses and the expression with its value and if a unit exists it adds that to the text.

An Example

Here is a complete example of a "master" .scss file:

@charset "UTF-8";

/*

 

P E R S O N A L S T Y L E S F O R D O C U M E N T S

 

 

 

 

*/

 

 

include Fonts.scss

 

define BaseLeftMargin 1.5em

define BaseRightMargin 1.5em

 

include BodyHnParagraph.scss

define BodyFont DejaVuSansCondensed, sans-serif

define HeadingFont DejaVuSans

define BodyBackgroundColour rgb(100%,100%,93%)

 

define H1Colour rgb(77%,15%,0%)

define H1Borders border-top: solid 0.15em rgb(67%,67%,33%); border-bottom: none

define H1BackgroundColour rgb(94%,94%,67%)

 

define H2Colour rgb(77%,15%,0%)

define H2Borders border-top: solid 0.1em rgb(73%,73%,40%)

define H2BackgroundColour rgb(100%,100%,87%)

 

define H3Colour rgb(71%,15%,0%)

define H3Borders border-top: solid 0.1em rgb(73%,73%,40%)

define H3BackgroundColour rgb(100%,100%,100%)

 

define H4Colour rgb(0%,0%,0%)

define H4Borders border: none

define H4BackgroundColour transparent

 

include Nav.scss

 

include HeaderSections.scss

define HeaderBackgroundColour rgb(94%,94%,67%)

define HeaderBorderColour rgb(67%,67%,33%)

 

include FooterSection.scss

define FooterBorderColour rgb(73%,73%,40%)

define FooterBackgroundColour rgb(94%,94%,67%)

 

include WidthsBordersAlignments.scss

 

include Tables.scss

define TableHeadBackgroundColour rgb(100%,91%,69%)

define TableFootBackgroundColour rgb(79%,100%,100%)

define TableCellGeneralBorderColour rgb(53%,53%,53%)

define TableCellGeneralBackgroundColour transparent

 

include Lists.scss

define ListLeftMargin 4.5em

 

include Rulers.scss

 

include Anchors.scss

define InPageAnchorDisplacement -5.0em

 

include ImagesFigures.scss

 

include Text.scss

define TextCodeColour rgb(0%,47%,0%)

define TextDefinitionColour rgb(47%,0%,69%)

define TextEmphasisColour rgb(60%,27%,0%)

define TextExampleColour rgb(13%,53%,0%)

define TextMathColour rgb(0%,0%,88%)

define TextQuoteColour rgb(20%,20%,50%)

define TextTermColour rgb(47%,0%,69%)

 

include Blocks.scss

define QuoteFramedColour rgb(20%,20%,50%)

 

include MathematicsAndProgramming.scss

 

/* END OF STYLE SHEET */

Operation

See the Manual.

LiveCode source

Here is the code for the stack:

/*

Purpose: to factor out common parts in CSS files.

Common parts are: css specifications and value definitions.

Source files of extension .scss (source CSS) are combined and then processed

to give normal style sheets of extension .css

A source file may contain two types of directive that will lead to processing:

include directives and define directives.

Syntax of directives:

include relative-file-path

define identifier value

Directives must be on a line by themselves.

Identifiers can have letters, digits and hyphens only.

File paths may contain spaces (there is no URL encoding).

Values may contain spaces and be expressions: the entire text following the space after the

identifier is the value.

Expressions may occur in the source if they are surrounded by {{ and }}.

There is no other restriction: e.g. it is possible to replace the word margin with padding.

Replacement is case sensitive.

Includes are only one level deep, i.e. includes inside included files are not processed.

The program processes one file at a time. The resulting style sheet is placed in a

file with the same name, and extension .css, located in the same folder as the source

file.

All directives are removed from the resulting CSS file.

First all includes are used to obtain one single text from several files, then all

@media rules are sorted so that there is only one rule per media type (since only one

is allowed) and all @media rules are put after all other rules. Then all

definitions are processed to replace identifiers with values in that single text.

Caveat: replacement of identifiers by values is just text replacement. It is done as a global

replacement, one identifier after another. Therefore if an identifier is a substring of the

value of another identifier which was replaced earlier, there may be unintended results.

The probability is low however, if some care is taken in composing the identifiers.

Known issues:

The byte order mark works for OSX only.

*/

1constant cBOM = "Ôªø" -- Byte Order Mark

2global gSource, xCH, nCH, CH

3global cEOF, cEOL

4global gToken, gTokens, gValue, gValues, gKind, gIdentifier

5global gError

6command CSSExpand fFile

7 put the milliseconds into t0

-- initialise "constant"s:

8 put numtochar(29) into cEOL; put numtochar(3) into cEOF

-- File system checks have been made, just extract the path and name again:

9 set the itemdelimiter to "/"

10 put (item 1 to -2 of fFile)&"/" into lPath

11 put last item of fFile into lFileName

12 set the itemdelimiter to "."

13 delete last item of lFileName

14 set itemdelimiter to ","

15 put WithNoBOM(url("file://"&fFile)) into lMainSource -- Assumed to be utf8 and can therefore be changed in its ASCII parts

-- First process the include directives:

16 put empty into lTextWithIncludes

17 repeat for each line iLine in lMainSource

18 if word 1 of iLine is "include" then

19 delete word 1 of iLine

20 put WithoutLeadingSpace(iLine) into lLine

21 put WithNoBOM(url("file://"&lPath&lLine))&LF after lTextWithIncludes

22 next repeat

23 end if

24 put iLine&LF after lTextWithIncludes

25 end repeat

26 put cBOM&lTextWithIncludes into url("binfile://"&lPath&lFileName&".css") -- so we can refer to wrong lines for debugging

-- Sort the @media rules:

27 put empty into lTextWithAtMediaSorted

-- look for @media, then count all { and } until the end of the @media rule

-- put the entire rule into an array element for that type

28 put empty into lMediaRule

29 put 1 into lp

30 repeat

31 put offset("@media",lTextWithIncludes) into lo

32 if lo=0 then exit repeat

33 if char lp of lTextWithIncludes is CR then add 1 to lp

34 put char lp to lo-1 of lTextWithIncludes after lTextWithAtMediaSorted

35 delete char 1 to lo-1 of lTextWithIncludes

36 put 1 into lBraceCount

37 delete word 1 of lTextWithIncludes -- the @media

38 put offset("{",lTextWithIncludes) into lo

39 put char 1 to lo-1 of lTextWithIncludes into lMediaTypes

40 delete char 1 to lo of lTextWithIncludes

41 if char 1 of lTextWithIncludes is CR then delete char 1 of lTextWithIncludes

42 put 0 into i

43 repeat

44 add 1 to i

45 if char i of lTextWithIncludes is "{" then add 1 to lBraceCount

46 if char i of lTextWithIncludes is "}" then

47 subtract 1 from lBraceCount

48 if lBraceCount = 0 then

49 exit repeat

50 end if

51 end if

52 end repeat

53 put char 1 to i-1 of lTextWithIncludes into lRule

54 repeat with j=1 to the number of items of lMediaTypes

55 put lRule after lMediaRule[Clean(item j of lMediaTypes)]

56 end repeat

57 put i+1 into lp

58 end repeat

59 put the keys of lMediaRule into lMedia

60 repeat for each line xLine in lMedia

61 put CR&"@media "&xLine& " {"&CR&lMediaRule[xLine]&"}"&CR after lTextWithAtMediaSorted

62 end repeat

63 put 0 into xValue

64 put empty into lValues -- Array of all quadruples identifier,type,value,unit, indexed by number xValue.

65 set the casesensitive to true

-- Gather the declarations:

66 put empty into lTextWithoutDefinitions

67 put 0 into lLineNumber

68 repeat for each line iLine in lTextWithAtMediaSorted

69 add 1 to lLineNumber

70 if word 1 of iLine is "define" then

71 put iLine into gSource

72 LexicalScan

73 if gError is not empty then

74 answer "Line "&lLineNumber&": "&gError as sheet

75 exit CSSExpand

76 end if

77 add 1 to xValue

78 put gIdentifier into lValues[xValue,"I"]

79 put empty into lValues[xValue,"T"]

80 put empty into lValues[xValue,"V"]

81 put empty into lValues[xValue,"U"]

82 delete word 1 to 2 of iLine

83 if gKind is "Unquoted String" then

84 put "string" into lValues[xValue,"T"]

85 repeat with i=1 to xValue-1 -- replace known identifiers with their values

86 if lValues[i,"I"] is in iLine then

87 if lValues[i,"T"] is "string" then

88 put lValues[i,"V"] into lValue

89 else

90 put lValues[i,"V"] into lValue

91 if lValues[i,"U"] is not empty then put lValues[i,"U"] after lValue

92 end if

93 replace lValues[i,"I"] with lValue in iLine

94 end if

95 end repeat

96 put WithoutLeadingSpace(iLine) into lValues[xValue,"V"]

97 next repeat

98 else if gKind is "Parameter" then

99 put "number" into lValues[xValue,"T"]

100 put line 1 of gValues into lValues[xValue,"V"]

101 put line 2 of gValues into lValues[xValue,"U"]

102 next repeat

103 else if gKind is "Alias" then

104 put false into lFound

105 put line 1 of gValues into lAliasID

106 repeat with i=1 to xValue-1 -- try to find the original

107 if lValues[i,"I"] is lAliasID then

108 put lValues[i,"T"] into lValues[xValue,"T"]

109 put lValues[i,"V"] into lValues[xValue,"V"]

110 put lValues[i,"U"] into lValues[xValue,"U"]

111 put true into lFound

112 exit repeat

113 end if

114 end repeat

115 if not lFound then -- do as if unquoted string

116 put "string" into lValues[xValue,"T"]

117 repeat with i=1 to xValue-1 -- replace known identifiers with their values

118 if lValues[i,"I"] is in iLine then

119 if lValues[i,"T"] is "string" then

120 put lValues[i,"V"] into lValue

121 else

122 put lValues[i,"V"] into lValue

123 if lValues[i,"U"] is not empty then put lValues[i,"U"] after lValue

124 end if

125 replace lValues[i,"I"] with lValue in iLine

126 end if

127 end repeat

128 put WithoutLeadingSpace(iLine) into lValues[xValue,"V"]

129 end if

130 next repeat

131 else if gKind is "Expression" then

132 put "number" into lValues[xValue,"T"]

133 try -- to evaluate the expression

134 put empty into lUnit

135 put "Number" into lFinalType

136 repeat with i=1 to xValue-1 -- replace known identifiers with their values

137 if lValues[i,"I"] is in iLine then

138 if lValues[i,"T"] is "string" then

139 put quote&lValues[i,"V"]&quote into lValue

140 put "string" into lFinalType

141 else

142 put lValues[i,"V"] into lValue

143 if lValues[i,"U"] is not empty then put lValues[i,"U"] into lUnit

144 end if

145 replace lValues[i,"I"] with lValue in iLine

146 end if

147 end repeat

-- put (expression) into Values[i,"V"]; put unit into Values[i,"U"]; put FinalType into Values[i,"T"]

148 put "put ("&iLine&") into lValues["&xValue&","&quote&"V"&quote&"]" into waste --debug

149 do "put ("&iLine&") into lValues["&xValue&","&quote&"V"&quote&"]"

150 do "put lUnit into lValues["&xValue&","&quote&"U"&quote&"]"

151 do "put "&quote&lFinalType&quote&" into lValues["&xValue&","&quote&"T"&quote&"]"

152 catch expressionerror

153 FailWith "Error in definition expression on line "&lLineNumber&": ",expressionerror

154 exit CSSExpand

155 end try

156 next repeat

157 end if

158 end if

159 put iLine&LF after lTextWithoutDefinitions

160 end repeat

161 put cBOM&lTextWithoutDefinitions into url("binfile://"&lPath&lFileName&".css") -- so we can refer to wrong lines for debugging

-- Evaluate the expressions in this text:

162 put empty into lTextWithExpressionsEvaluated

163 put 0 into lP; put 0 into lQ -- Character pointers into lTextWithoutDefinitions

164 repeat

165 put offset("{{",lTextWithoutDefinitions) into lP

166 if lP=0 then exit repeat

167 put char 1 to lP-1 of lTextWithoutDefinitions after lTextWithExpressionsEvaluated

168 put offset("}}",lTextWithoutDefinitions) into lQ

169 put char lP+2 to lQ-1 of lTextWithoutDefinitions into lExpression

170 delete char 1 to lQ+1 of lTextWithoutDefinitions

171 try -- to evaluate the expression

172 put empty into lUnit

173 put "Number" into lFinalType

174 repeat with i=1 to xValue -- replace known identifiers with their values

175 if lValues[i,"I"] is in lExpression then

176 if lValues[i,"T"] is "string" then

177 put quote&lValues[i,"V"]&quote into lValue

178 put "string" into lFinalType

179 else

180 put lValues[i,"V"] into lValue

181 if lValues[i,"U"] is not empty then put lValues[i,"U"] into lUnit

182 end if

183 replace lValues[i,"I"] with lValue in lExpression

184 end if

185 end repeat

186 if lFinalType is "Number" then

187 set the numberformat to "0.####"

188 do "put ("&lExpression&") into lValue"

189 put lValue&lUnit after lTextWithExpressionsEvaluated

190 else

191 do "put ("&lExpression&") into lValue"

192 put lValue after lTextWithExpressionsEvaluated

193 end if

194 catch expressionerror

195 FailWith "Error in {{expression}} on line "&(the number of lines of lTextWithExpressionsEvaluated)&": ",expressionerror

196 exit CSSExpand

197 end try

198 end repeat

199 put lTextWithoutDefinitions after lTextWithExpressionsEvaluated

-- Substitute strings and numbers:

-- To eliminate errors due to identifiers being substrings of other identifiers, first

-- sort by the identifiers by length descending.

-- (Note: this does NOT eliminate errors due to identifiers being substrings of values of other identifiers.

200 put empty into lIdentifiers; set the itemdelimiter to comma

201 repeat with i=1 to xValue

202 put i &comma& length(lValues[i,"I"]) &comma& lValues[i,"I"] &CR after lIdentifiers

203 end repeat

204 sort lines of lIdentifiers numeric descending by item 2 of each

205 repeat with i=1 to xValue

206 put item 1 of line i of lIdentifiers into j

207 if lValues[j,"T"] is "String" then

208 replace lValues[j,"I"] with lValues[j,"V"] in lTextWithExpressionsEvaluated

209 else

210 replace lValues[j,"I"] with (lValues[j,"V"]&lValues[j,"U"]) in lTextWithExpressionsEvaluated

211 end if

212 end repeat

-- Write the file:

213 put cBOM&lTextWithExpressionsEvaluated into url("binfile://"&lPath&lFileName&".css")

214 put lPath&lFileName&".css" into field "FilePath"

215 send "ShowTime "&quote&(the milliseconds -t0)&quote to field "Time"

216end CSSExpand

/*

Lexical Scan

Four cases are distinguished:

parameter: (number unit) or (operator- number unit)

alias: a single identifier that was defined earlier

expression: if no undefined and no units

unquoted string: anything else

*/

217command LexicalScan

218 put empty into gTokens; put empty into gValues; put empty into gError

219 replace return with space in gSource

220 put length(gSource) into nCH; put 0 into xCH

221 GetCH;

222 put empty into gToken

223 put empty into gKind

224 repeat

225 GetToken

226 if gToken = cEOF then exit repeat

227 put gToken&CR after gTokens

228 put gValue&CR after gValues

229 end repeat

230 if (line 1 of gTokens is "identifier") and (line 1 of gValues is "define") then

-- first token must be an identifier

231 if line 2 of gTokens = "identifier" then

232 put line 2 of gValues into gIdentifier

233 delete line 1 to 2 of gTokens; delete line 1 to 2 of gValues

234 switch

-- Parameter

235 case gTokens = "number"&CR&"unit"&CR

236 put "Parameter" into gKind

237 break

238 case gTokens="operator"&CR&"number"&CR&"unit"&CR and line 1 of gValues ="-"

239 put "Parameter" into gKind

240 put "-" before line 2 of gValues

241 delete line 1 of gTokens; delete line 1 of gValues

242 break

-- Expression

243 case the number of lines of gTokens > 1 and lineoffset("undefined",gTokens)=0 and lineoffset("unit",gTokens)=0 and "operator" is among the lines of gTokens

244 put "Expression" into gKind

245 break

-- Alias

246 case the number of lines of gTokens = 1 and line 1 of gTokens = "identifier"

247 put "Alias" into gKind

248 break

-- Unquoted string

249 default

250 put "Unquoted String" into gKind

251 break

252 end switch

253 else

254 put "Must define an identifier" into gError

255 end if

256 else

257 put "Strange error!" into gError

258 end if

259end LexicalScan

260command GetToken

-- read the next token.

261 put empty into gValue

262 repeat

263 switch

264 case CH is in "abcdefghijklmnopqrstuvwxyz" -- identifier

265 put "identifier" into gToken

266 put CH into gValue

267 GetCH

268 repeat while CH is in "abcdefghijklmnopqrstuvwxyz-0123456789"

269 put CH after gValue

270 GetCH

271 end repeat

272 if gValue is among the items of "em,en,pt,px,mm,cm,in" then

273 put "unit" into gToken

274 end if

275 exit repeat

276 break

277 case CH is in ".0123456789" -- number

278 put "number" into gToken

279 put CH into gValue

280 GetCH

281 repeat while CH is in "0123456789."

282 put CH after gValue

283 GetCH

284 end repeat

285 exit repeat

286 break

287 case CH is in "+-*/&" -- operator

288 put "operator" into gToken

289 put CH into gValue

290 GetCH

291 exit repeat

292 break

293 case CH is quote -- string

294 put "string" into gToken

295 put empty into gValue

296 GetCH

-- read everything until the ending quote, skip double quotes.

297 repeat -- until End Of String

298 if CH is quote then -- maybe the end?

299 GetCH

300 if CH is quote then -- no, a doubled quote, store one

301 put CH after gValue

302 GetCH

303 else -- end of string

304 exit repeat

305 end if

306 else

307 put CH after gValue

308 GetCH

309 end if

310 if CH is cEOF then exit repeat

311 end repeat

312 exit repeat

313 break

314 case CH is "("

315 put "open" into gToken

316 GetCH

317 exit repeat

318 break

319 case CH is ")"

320 put "close" into gToken

321 GetCH

322 exit repeat

323 break

324 case CH is in space&tab -- white space

325 GetCH

326 break

327 case CH = cEOF -- --------------end of string

328 put cEOF into gToken

329 exit repeat

330 break

331 default

332 put empty into gUndefined

333 put "undefined" into gToken

334 put CH after gValue

335 GetCH

336 exit repeat

337 break

338 end switch

339 end repeat

340end GetToken

341command GetCH

-- Returns the next character from the source in global variable CH.

-- xCH points to the last character returned.

342 add 1 to xCH

343 if xCH <= nCH then -- normal case

344 put char xCH of gSource into CH

345 else -- reached the end of the text

346 put cEOF into CH

347 end if

348end GetCH

349function WithNoBOM fS

350 if char 1 to 3 of fS = cBOM then

351 return char 4 to -1 of fS

352 else

353 return fS

354 end if

355end WithNoBOM

356function WithoutLeadingSpace fS

357 repeat while char 1 of fS is in space&tab

358 delete char 1 of fS

359 end repeat

360 return fS

361end WithoutLeadingSpace

362command FailWith fMessage,fError

363 replace return with comma in fError

364 repeat while item 1 of fError is a number

365 delete item 1 of fError

366 end repeat

367 answer fMessage&fError as sheet

368end FailWith

369command PrepareForStandAlone

370 put empty into field "FilePath"

371end PrepareForStandAlone

372function Clean fs

-- replace returns and tabs with spaces, remove leading and trailing spaces

373 replace return with space in fs; replace tab with space in fs

374 repeat while char 1 of fs is space

375 delete char 1 of fs

376 end repeat

377 repeat while char -1 of fs is space

378 delete char -1 of fs

379 end repeat

380 return fs

381end Clean

This is the code for the field:

1on DragDrop

-- Do the file system checks

2 put the dragdata["files"] into lFile

3 if lFile is empty then exit DragDrop

4 if the number of lines of lFile > 1 then

5 answer "Only one file at a time please." as sheet

6 exit DragDrop

7 end if

8 set the itemdelimiter to "/"

9 put (item 1 to -2 of lFile)&"/" into lPath

10 put last item of lFile into lFileName

11 set the itemdelimiter to "."

12 if last item of lFileName is not "scss" then

13 answer "File extension must be scss" as sheet

14 exit DragDrop

15 end if

16 delete last item of lFileName

17 if there is a file (lPath&lFileName&".css") and the optionkey is up then

18 answer lFileName&".css exists already, overwrite?" with "Yes" or "No" as sheet

19 if it is "No" then

20 exit DragDrop

21 end if

22 end if

23 put empty into me

24 CSSExpand lFile

25end DragDrop

26on MouseUp

27 if me is empty then exit MouseUp

28 put me into lFile

29 set the itemdelimiter to "/"

30 put (item 1 to -2 of lFile)&"/" into gPath

31 put last item of lFile into lFileName

32 set the itemdelimiter to "."

33 delete last item of lFileName

34 put lFileName into gFileName

35 put empty into me

36 CSSExpand lFile

37end MouseUp