CircleScript: Content Replacement and Concatenation
This article is a syntax reference for CircleScript's core content assembly features, including tag syntax, $using directives, local definitions, iteration, and the .cmf file format.
If you are new to CircleScript, read the CircleScript Overview first to understand the underlying concepts before working through the syntax here.
In this article
- Tag syntax
- $using directives
- Local definitions
- Defaults: coalescence and guard
- Iteration with $each
- The .cmf file format
- Other supported data formats
Tag syntax
CircleScript uses [$path] syntax to reference content. The path can point to a file in the CMS or to a locally defined variable.
| Syntax | What it does | Example |
| [$path] | Reference a file in the CMS or a local definition | [$/headers/myheader.html] |
| [$functionName(args)] | Call a built-in CircleScript function | [$upper([$first-name])] |
| [$$$...$$] | Define a nested scope | [$$$color=blue [$content] $$] |
$using directives
The $using directive makes a CMS directory or data file available for tag resolution within the current scope. Once declared, tags are resolved against the specified path before looking elsewhere.
| $using /headers |
| [$myheader.html] |
| -> contents of "/headers/myheader.html" |
Multiple $using directives can be stacked. If the same key is defined in more than one file, the last $using directive takes precedence within the current scope.
| $using branding.cmf // defines colors, images, fonts |
| $using fall-colors.cmf // overrides colors defined in branding |
$using directives stack and can be scoped. A $using declared inside a nested scope [$$$...$$] applies only within that scope.
Local definitions
Local definitions create variables within the current content scope. They are available from the point of definition through the end of the current scope.
| $game=chess |
| Thanks for playing [$game]! |
| -> Thanks for playing chess! |
Variables defined in a nested scope do not affect the outer scope.
| $color=black |
| [$$$color=blue |
| Color inside scope: [$color] // blue |
| $$] |
| Color outside scope: [$color] // black |
Defaults: coalescence and guard
Coalescence
Use the ?? operator to provide a fallback value when an expression is blank or missing.
Syntax:
| <expression>??<fallback> |
Example:
| $name=[$first-name]??friend |
| Hello [$name]! |
| -> Hello friend! // when first-name is blank |
Coalescence expressions can be chained:
| $display=[$nickname]??[$first-name]??friend |
Guard
Use the ? operator to include content only when an expression is not blank.
Syntax:
| ?<guard-expression>:<conditional-content> |
Example:
| $level_message=?[-level-]:You are at level [-level-]! |
| [$level_message] |
| -> You are at level 7! // when level = 7 |
| -> (blank) // when level is blank |
AND and OR logic
Guard and coalesce syntax can be combined to implement simple boolean logic. Blank represents false; any non-blank value represents true.
AND example:
| $p1=true |
| $p2=true |
| $p3= |
| $p1and2=?[$p1]:?[$p2]:yes??no |
| $p2and3=?[$p2]:?[$p3]:yes??no |
| 1 and 2 : [$p1and2] // yes |
| 2 and 3 : [$p2and3] // no |
OR example:
| $p1=true |
| $p2= |
| $p1or2=?[$p1][$p2]:yes??no |
| 1 or 2 : [$p1or2] // yes |
Iteration with $each
The $each directive repeats a block of content for each item in a range or sequence. It stops automatically once its block produces no output (short-circuit evaluation).
Basic iteration
| $each i:[1..3] |
| Repeat me! |
| -> Repeat me!Repeat me!Repeat me! |
Iterator in path
| $each i:[1..3] |
| [$content_[$i]] |
| -> [$content_1][$content_2][$content_3] |
Implicit path iteration (shorthand)
[$content_*] is shorthand for [$content_[$i]] when used inside a $each directive.
| $each content_[1..3] |
| [$content_*] |
| -> [$content_1][$content_2][$content_3] |
Qualifying the each block
Use a guard expression to skip blank items and stop iteration cleanly.
| $each content_[1..100] |
| ?[$content_*]:<p>[$content_*]</p> |
| -> <p>[$content_1]</p><p>[$content_2]</p>... stops when [$content_N] is blank |
Iteration in a sub-context
To loop through a block in the middle of other content, place the repeated block in a nested scope with a $each directive on the first line.
| <table> |
| [$$$each row:[1..100] |
| ?[$c[$row]_1]:<tr>[$$$each col:[1..100] |
| ?[$c[$row]_[$col]]:<td>[$c[$row]_[$col]]</td>$$]</tr> |
| $$]</table> |
| -> <table> |
| <tr><td>[$c1_1]</td><td>[$c1_2]</td>...</tr> |
| <tr><td>[$c2_1]</td><td>[$c2_2]</td>...</tr> |
| ...</table> |
The .cmf file format
The .cmf (Content Manager Format) file is the primary format for storing key-value definitions used in CircleScript content assembly. It supports both short single-line definitions and multi-line HTML blocks.
Short definitions
Each definition starts in the first column with an identifier followed by an equals sign, with the value on the same line.
| first-name=Sarah |
| product=Premium Membership |
| promo-code=SAVE20 |
Multi-line HTML blocks
When the equals sign is followed by a newline with no value on the same line, the value is pulled from the following lines until the next definition or the end of the file.
| hero-banner= |
| <div class='hero'> |
| <h1>Welcome back, [$first-name]!</h1> |
| <p>Your exclusive offer: [$promo-code]</p> |
| </div> |
| footer-text= |
| <p>Unsubscribe at any time.</p> |
| IMPORTANT If an attribute name or HTML element starts in the first column of a .cmf file, it will be interpreted as a new key definition rather than content. Indent HTML element attributes to avoid this. |
Using a .cmf file in content
| $using /data/mydata // references /data/mydata.cmf |
| [$hero-banner] |
| [$footer-text] |
Other supported data formats
CircleScript can also parse .csv, .tsv, .json, and .xml files into name-value pairs for use in content assembly.
.csv and .tsv
Tabular data is converted to name-value pairs using column headers. Columns that represent unique keys must be annotated with (key) in the header row.
| // Header row: |
| event_id(key), title, description, location, date |
| // First data row: |
| E1, First Event, The first event, Boise Idaho, 2023-10-01 |
| // Produces: |
| E1.event_id=E1 |
| E1.title=First Event |
| E1.description=The first event |
| E1.location=Boise Idaho |
| E1.date=2023-10-01 |
.json
JSON objects and arrays are traversed and converted to name-value pairs by building up keys as the structure is parsed.
| // Input: {a:1, b:2, c:3} |
| // Produces: a=1, b=2, c=3 |
| // Input: {id: [1,2,3]} |
| // Produces: id1=1, id2=2, id3=3 |
.xml
XML is parsed into name-value pairs. Both element content and nested structures are accessible.
| // Input: |
| <p><div><tt>foo</tt></div></p> |
| // Produces: |
| p1 = <p><div><tt>foo</tt></div></p> |
| p1.div1 = <div><tt>foo</tt></div> |
| p1.div1.inner = <tt>foo</tt> |
| p1.div1.text = foo |
| p1.div1.tt1 = <tt>foo</tt> |
| p1.div1.tt1.inner = foo |
| p1.div1.tt1.text = foo |