Purpose
To factor out common parts in CSS files.
Program executable and source can be downloaded from links on the page CSS Factoring Manual
en
To factor out common parts in CSS files.
Program executable and source can be downloaded from links on the page CSS Factoring Manual
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.
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 4π. 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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
Tokens | Values |
---|---|
number | 3.5 |
unit | em |
But for -12px it returns:
Tokens | Values |
---|---|
operator | - |
number | 12 |
unit | px |
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).
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.
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 */
See the Manual.
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"]"e 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&",""e&"V""e&"]" into waste --debug
149 do "put ("&iLine&") into lValues["&xValue&",""e&"V""e&"]"
150 do "put lUnit into lValues["&xValue&",""e&"U""e&"]"
151 do "put ""e&lFinalType"e&" into lValues["&xValue&",""e&"T""e&"]"
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"]"e 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 ""e&(the milliseconds -t0)"e 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