Syntax
#Script aims to be a familiar and expressive dynamic language for scripting .NET Apps, that is optimal at generating text, especially HTML where it's pre-configured with the HTML Page Format and HTML Scripts but unlike C#'s Razor can support multiple pluggable page formats which can be used for generating any kind of text.
For maximum familiarity #Script uses JavaScript Expressions that for increased readability and expressiveness supports being used within Template Expressions which like Vue.js filters, and Angular's Template Expressions lets you use the | pipe operator to chain the return values of methods into subsequent methods from left-to-right. For statements #Script adopts the familiar Handlebars-like Script Blocks that is also popular among HTML templating engines.
Effectively #Script lets you use the same familiar JS language for server HTML rendering as you would do in client-side rendering of Single Page Apps despite it binding natively to C# objects and calling C# methods behind-the-scenes.
Mustache expressions
Like Vue/Angular Templates, only expressions inside mustaches are evaluated, whilst everything outside are emitted as-is:
Which calls the upper default script method where the argument on the left-side of the "pipe" symbol is passed as the first argument to the upper method which is implemented as:
public string upper(string text) => text?.ToUpper();
This can be rewritten without the "pipe forward" operator by calling it as a method or an extension method instead:
Methods can be chained
Methods are chained from left-to-right where the value on the left side of the "pipe" symbol is passed as the first argument in the method on the right and the output of that is passed as the input of the next method in the chain and so on:
Methods can also accept additional arguments which are passed starting from the 2nd argument since the first argument is the value the method is called with. E.g. here are the implementations for the substring and padRight default scripts:
public string substring(string text, int startIndex) => text.SafeSubstring(startIndex);
public string padRight(string text, int totalWidth, char padChar) => text?.PadRight(totalWidth, padChar);
JavaScript literal notation
You can use the same literal syntax used to define numbers, strings, booleans, null, Objects and Arrays in JavaScript within templates and it will get converted into the most appropriate .NET Type, e.g:
ES6 Shorthand notation is also supported where you can use the argument name as its property name in a Dictionary:
Local Variables
Like JavaScript you can declare a locally scoped variable with var:
var sliders = dirFiles('img/sliders')
Like JS you can use either var, let or const but they all behave like let and assign a locally scoped variable at the time the expression is executed. Also like JS the semicolon is optional and you can assign multiple variables in a single expression:
let a = 1 + 2, b = 3 * 4, c, d = 'D';
Global Variables
Global Variables in #Script are maintained in the PageResult.Args dictionary which you could previously assign to using the toGlobal script method where it’s accessible to all scripts rendered within that PageResult.
#Script now mimics JS's behavior to allow assignment expressions to assign global variables where **Assignment Expressions** on undeclared variables (i.e. where no locally scoped variable exists) will assign a global variable:
a = 1
A more descriptive syntax available to declare a global variable is to assign it to the global object (inspired by node’s global) which is an alias to the PageResult.Args dictionary:
global.a = 1
Assignment Expressions
In addition to declaring and assigning variables, there’s also support for using assignment expressions to assign and mutate Collections and Type Properties using either Member Expression or Index expression syntax, e.g:
intList[1] = 10
stringArray[1] = "foo"
stringMap["foo"] = "bar"
person.Age = 27
objectMap.Person.Name = "kurt"
objectMap['Per' + 'son'].Name = "kurt"
intList[1.isOdd() ? 2 : 3] = 30
Dynamically resolve args
The resolve*
APIs let you resolve a variable named with the result of an expression, e.g:
var tableNames = resolveGlobal(`${db}_tables`)
Equivalent to:
var tableNames = global[`${db}_tables`]
Whereas resolveArg
lets you resolve a variable using locally scope resolution hierarchy:
var tableNames = resolveArg(`${db}_tables`)
Quotes
Strings can be defined using single quotes, double quotes, prime quotes or backticks:
Strings can also span multiple lines.
Template Literals
Backticks strings implement JavaScript's Template literals which can be be used to embed expressions:Shorthand arrow expression syntax
#Script Expressions have full support for JavaScript expressions but doesn't support statements or function declarations although it does support JavaScript's arrow function expressions which can be used in functional methods to enable LINQ-like queries. You can use fat arrows => immediately after methods to define lambda's with an implicit (it => ...) binding, e.g:
This is a shorthand for declaring lambda expressions with normal arrow expression syntax:
Using normal lambda expression syntax lets you rename lambda parameters as seen in the map(x => ...) example.
Special string argument syntax
As string expressions are a prevalent in #Script, we've also given them special wrist-friendly syntax where you can add a colon at the end of the method name which says to treat the following characters up until the end of the line or mustache expression as a string, trim it and convert '{' and '}' chars into mustaches. With this syntax you can write:
and it will be rewritten into its equivalent and more verbose form of:
SQL-like Boolean Expressions
To maximize readability and intuitiveness for non-programmers, boolean expressions can also adopt an SQL-like syntax where instead of using && or || operator syntax to define boolean expressions you can also use the more human-friendly and and or alternatives:
Include Raw Content Verbatim
Use #raw blocks to ignore evaluating expressions and emit content verbatim. This is useful when using a client Handlebars-like templating solution like Vue or Angular templates where expressions need to be evaluated with JavaScript in the browser instead of on the Server with Templates:Multi-line Comments
Any text within {{#noop}} ... {{/noop}} block statements are ignored and can be used for temporarily removing sections from pages without needing to delete it.
Everything within multi-line comments {{* and *}} is ignored and removed from the page.
An alternative way to temporarily disable an expression is to prefix the expression with the end method to immediately short-circuit evaluation, e.g: {{ end |> now |> dateFormat }}
See Ignoring Pages for different options for ignoring entire pages and layout templates.
Script Methods can be used as Extension Methods
A core feature of #Script
is that it runs in a sandbox and only has access to functionality that's configured in its ScriptContext
that it runs in, so by design #Script
is prohibited from calling instance methods so they only have a read-only view of your objects
unless you explicitly register ScriptMethods
that allows them to change them.
This frees up the instance.method()
syntax to be put to other use which can now be used to call every script method as an extension method.
This can greatly improve the readability and execution flow of code, e,g. we can rewrite our previous
JS Utils Eval example:
itemsOf(3, padRight(reverse(arg), 8, '_'))
into the more readable form using the same methods as extension methods off the first argument, e.g:
3.itemsOf(arg.reverse().padRight(8, '_'))
As an example, C#'s 101 LINQ examples most complicated LINQ expression can now be rewritten from its original source:
to use extension methods which greatly improves its readability as its execution flow is now able to read from left-to-right:
JavaScript Array Support
We can use extension methods to define instance methods that can be called on any object to implement JS Array methods support
to further improve #Script
source compatibility with JavaScript.
Here are Mozilla's Array examples in #Script
utilizing its Code Blocks feature to reduce the amount of boilerplate required:
```code
`Create an Array`
['Apple', 'Banana'] |> to => fruits
fruits.Count
`\nAccess (index into) an Array Item`
fruits[0] |> to => first
first
`\nLoop over an Array`
#each item in fruits
`${item}, ${index}`
/each
[] |> to => sb
fruits.forEach((item,index,array) => sb.push(`${item}, ${index}`))
sb.join(`\n`)
`\nAdd to the end of an Array`
fruits.push('Orange') |> to => newLength
newLength
`\nRemove from the end of an Array`
fruits.pop() |> to => last
last
`\nRemove from the front of an Array`
fruits.shift() |> to => first
first
`\nAdd to the front of an Array`
fruits.unshift('Strawberry') |> to => newLength
newLength
`\nFind the index of an item in the Array`
fruits.push('Mango') |> end
fruits.indexOf('Banana') |> to => pos
pos
`\nRemove an item by index position`
fruits.splice(pos, 1) |> to => removedItem
removedItem |> join
fruits |> join
`\nRemove items from an index position`
['Cabbage', 'Turnip', 'Radish', 'Carrot'] |> to => vegetables
vegetables |> join
1 |> to => pos
2 |> to => n
vegetables.splice(pos, n) |> to => removedItems
vegetables |> join
removedItems |> join
`\nCopy an Array`
fruits.slice() |> join
```
Which can be run with x run {script}.ss
to view its expected output:
Create an Array
2
Access (index into) an Array Item
Apple
Loop over an Array
Apple, 0
Banana, 1
Apple, 0
Banana, 1
Add to the end of an Array
3
Remove from the end of an Array
Orange
Remove from the front of an Array
Apple
Add to the front of an Array
2
Find the index of an item in the Array
1
Remove an item by index position
Banana
Strawberry,Mango
Remove items from an index position
Cabbage,Turnip,Radish,Carrot
Cabbage,Carrot
Turnip,Radish
Copy an Array
Strawberry,Mango
Most JS Array methods are supported, including the latest additions from ES2019:
concat
every
filter
find
findIndex
flat
flatMap
forEach
includes
indexOf
join
keys
lastIndexOf
map
pop
push
reduce
reverse
shift
slice
some
sort
splice
toString
unshift
values
Language Blocks and Expressions
We've caught a glimpse using language blocks with the code
JavaScript Array Example above which allows us to invert #Script
from "Template Mode"
where all text is emitted as-is with only code within Template Expressions {{ ... }}
are evaluated and changed it to "Code Mode" where all code
is evaluated a code expression.
This is akin to Razor's statement blocks which inverts Razor's mode of emitting text to treating text inside statement blocks as code, e.g:
@{
var a = 1;
var b = 2;
}
Basic Calc
a + b = @(a + b)
The equivalent in #Script
using code
language blocks:
```code
var a = 1
var b = 2
```
Basic Calc
a + b = {{a + b}}
Which is useful in reducing boilerplate when you need to evaluate code blocks with 2+ or more lines without the distracting boilerplate of wrapping
each expression within a {{ ... }}
Template Expression.
Languages
Refer to the languages page to learn about alternative languages you can use within Language Blocks and Expressions:
- #Script Code - use
code
language identifier - #Script Lisp - use
lisp
language identifier
Language Blocks
code
fragments are executed using #Script
language blocks in the format:
```<lang>
```
Where #Script
will parse the statement body with the language registered in its ScriptContext's ScriptLanguages
collection that's pre-registered
with ScriptCode.Language
which is used to process code
block statements, e.g:
{{ 1 |> to => a}}
```code
(a + 2) |> to => result
```
Code result: {{ result }}
Code Statement Blocks are evaluated within the same scope so any arguments that are assigned are also accessible within the containing page as seen above.
Evaluate Lisp
You can use language blocks to embed and evaluate any of the languages registered in your ScriptContext, e,g. to evaluate #Script Lisp register it in your ScriptContext or SharpPagesFeature:
var context = new ScriptContext {
ScriptLanguages = { ScriptLisp.Language },
}.Init();
Where it will let you use Language Blocks to evaluate LISP code within #Script:
{{ 1 |> to => a}}
```lisp
(def local-arg (+ a 2))
(export result local-arg)
```
Lisp result: {{ result }}
Unlike code
blocks, Lisp evaluates its code using its own Symbols table, it's able to reference arguments not in its Global symbols
by resolving them from the containing scope, but in order for the outer #Script
to access its local bindings they need
to be exported as seen above which registers the value of its local-arg
value into the result
argument.
Language Block Modifiers
You can provide any additional modifiers to language blocks by specifying them after them after the |
operator, languages
can use these modifiers to change how it evaluates the script. By default the only modifiers the built-in languages support are |quiet
and its shorter |mute
and |q
aliases which you can use to discard any output from being rendered within the page.
If you use Lisp's setq special form to assign a variable, that value is also returned which would be rendered in the page, you can ignore this output by using one of the above modifiers, e.g:
{{ 1 |> to => a}}
```lisp|q
(setq local-arg (+ a 2))
(export result local-arg)
```
Lisp result: {{ result }}
Language Expressions
If you wanted to instead embed an expression in a different language instead of executing an entire statement block,
you can embed them within Language Expressions {|<lang> ... |}
, e.g:
You could also use them to evaluate code
expressions, e.g. {|code 1 + 2 |}
, but that's also how #Script
more concise Template Expressions are evaluated {{ 1 + 2 }}
.
Multi-language support
Despite being implemented in different languages a #Script
page containing multiple languages, e.g:
Still only produces a single page AST, where when first loaded #Script
parses the page contents as a contiguous
ReadOnlyMemory<char>
where page slices of any Language Blocks and Expressions
on the page are delegated to the ScriptContext
registered ScriptLanguages
for parsing which returns a fragment which is
added to the pages AST:
When executing the page, each language is responsible for rendering its own fragments which all write directly to the pages OutputStream
to generate the pages output.
The multi-languages support in #Script
is designed to be extensible where everything about the language is encapsulated within its
ScriptLanguage
implementation so that if you omit its registration:
var context = new ScriptContext {
// ScriptLanguages = { ScriptLisp.Language }
}.Init();
Any language expressions and language blocks referencing it become inert and its source code emitted as plain-text.
Language Comparisons
Whilst all languages have access to the same Script Methods and .NET Scripting interop, they all have unique characteristics that make them suitable for different use-cases. The default template syntax is ideal for generating text output where you need to embed logic when generating text output, #Script Code is ideal when logic is more important then the text output like in Sharp Scripts, whilst #Script Lisp is ideal for defining algorithms or for developers who prefer Lisp syntax or for devs who prefer development in a functional style.
A good comparison showing the differences, strengths and weaknesses of each language can be seen with the implementation of FizzBuzz in each language:
Template
#Script
is a "template-first" language where only expressions within {{ ... }}
literals are evaluated, everything outside
it is emitted verbatim, as consequence it's white-space sensitive where the indention of logic blocks affect its output as
seen above where all lines are indented with the same 2 spaces as its free-text contents.
Code
By contrast #Script Code is an inverted Templates where it's a "code-first" language where all statements are treated as code blocks
so statements don't require the {{ ... }}
boilerplate and are not white-space sensitive. Like Templates each content block
still generates output (unless its suppressed with the quiet modifier) but needs to be a valid expression
like a string literal, argument or expression as seen in the code example above.
Lisp
#Script Lisp lets you use the venerable and highly dynamic and functional Lisp language that's particularly strong at capturing algorithms and composing functions, although FizzBuzz isn't a particularly good showcase of either of its dynamism of functional strengths, the last line shows a glimpse of its natural functional composition.
Combine strengths of all Languages
As all languages compile down into the same AST block we can freely use all languages within the same Script block to take advantage of the strengths of each, e.g. Template is great at generating text and Lisp excels at algorithms which make a great combination if you ever need to combine the 2, so you could easily for example use the defn Script Block to define a function in Lisp, compiled to a .NET delegate so its invocable as a regular function inside a Template Block that generates markdown captured by the capture Script Block and converted to HTML using the markdown filter transformer: