Tutorial
title: TabScript Tutorial
Section titled “title: TabScript Tutorial”TabScript Tutorial
Section titled “TabScript Tutorial”TabScript is an alternate syntax for TypeScript that replaces braces with indentation and introduces shorthand operators while maintaining full TypeScript compatibility. The compiler outputs clean TypeScript or JavaScript.
Getting Started
Section titled “Getting Started”Let’s start with a complete example that showcases TabScript’s clean syntax:
tabscript 2.0
interface Task title: string status: "done" or "pending" priority: number
# Arrow function with := (const)filterTasks := |tasks: Task[], status: "done" or "pending"| tasks.filter.. |t| t.status == status and t.priority > 0
# Single expression functionsgetHighPriority := |tasks: Task[]| tasks.filter.. |t| t.priority >= 8
# Named functionfunction printTaskStats|tasks: Task[]| completed := filterTasks.. tasks "done" pending := filterTasks.. tasks "pending"
# for-of with : declares a const for task: of getHighPriority(pending) console.log("HIGH PRIORITY:", task.title)
if completed.length > 0 console.log(`Completed ${completed.length} tasks!`)A few things to note:
- Header declaring the TabScript version.
- Tab indentation (no spaces, hence the name) defines code blocks.
- Declarations use
:=forconstand::=forlet. - Functions are defined by wrapping ’||’ around the parameters.
- Equality operators are strict by default (
==means===).
Variables
Section titled “Variables”Variable declarations use colons: a single : for const and a double :: for let.
tabscript 2.0
# One colon means constx : number = 3z := 42
# Two colons means lety :: string = "hello"w ::= 42
# Declaration without initial valuearr : number[]
# Union types (use 'or' instead of |)value : string or undefinedFunctions
Section titled “Functions”Functions use || to wrap parameters instead of (). For arrow functions, you can omit braces when returning an expression.
Note that we’re leaving out the required tabscript 2.0 header in the following examples for brevity.
# Arrow functionsadd := |a, b| a + bdouble := |x: number| x * 2
# Async arrow functionfetch := async |url| await loadData(url)
# Named function with single expressionfunction greet|name| `Hi ${name}`
# Named function with block bodyfunction calculate|a: number, b: number| result := a + b return result
# Generic functionidentity := <T>|x: T| xFunction Calls
Section titled “Function Calls”Use .. to call functions with space-separated arguments or one argument per line for cleaner syntax.
# Regular call (traditional syntax still works)result := func(a, b)
# Call with .. and space-separated argsresult := func.. a b
# Call with .. and indented argsresult := func.. a b
# Passing in an anonymous function as argumentprocessData.. options |item| item.value *= 2Control Flow
Section titled “Control Flow”All control structures use indentation instead of braces.
# If statement (single line)if x > 0 console.log("positive")
# If statement (block body)if x > 0 console.log("positive") x++
# If-elseif x > 0 console.log("positive")else console.log("not positive")
# While loopwhile i < 10 i++
# For-of loop with type-inferred constantfor item: of array console.log(item)
# For-in loopfor key: in obj console.log(key, obj[key])
# C-style for loop with letfor i ::= 0; i < 10; i++ console.log(i)
# Switch (values don't need 'case' keyword)switch day 1 console.log("Monday") 2 console.log("Tuesday") * console.log("Other day")
# Try-catchtry riskyOperation()catch error console.log(error)
# Or without the catch, and on a single linetry riskyOperation()Operators
Section titled “Operators”Logical Operators
Section titled “Logical Operators”TabScript uses and and or for logical operators instead of && and ||.
if x > 0 and y > 0 console.log("both positive")
if x == 0 or y == 0 console.log("at least one zero")Equality
Section titled “Equality”TabScript uses == and != for strict equality (like TypeScript’s === and !==). For loose equality, use =~ and !~.
# Strict equality by defaultif x == y console.log("equal")
if x != y console.log("not equal")
# Explicit loose equalityif x =~ y console.log("loosely equal")Null/Undefined Check
Section titled “Null/Undefined Check”Test if an expression is neither null nor undefined by suffixing it with ?.
if getValue()? console.log("has value")Binary Operators
Section titled “Binary Operators”Binary operators and modulo use verbose names with a % prefix. They should be relatively uncommon, so this makes their use more explicit and frees up symbols for other, more frequently used, constructs.
# Bitwise operationsresult := x %bit_or yresult := x %bit_and yresult := x %bit_xor yresult := %bit_not x
# Bit shiftsresult := x %shift_left 2result := x %shift_right 2result := x %unsigned_shift_right 2
# Moduloconsole.log(5 %mod 3, "equals 2")Classes
Section titled “Classes”Classes use indentation-based syntax. Methods need || even when they have no parameters.
# Basic class with propertiesclass Person name: string age: number
# Constructor with parameter propertiesclass Person constructor| public name: string private age: number | ;
# Methods (|| means no parameters)class Person greet|| return "Hello"
setAge|age: number| this.age = age
# Getters and settersclass Person get name|| return this._name
set name|value| this._name = value
# Inheritanceclass Dog extends Animal implements Pet makeSound|| console.log("Woof!")
# Generic classclass Box<T> value: T constructor|value: T| this.value = valueTypes and Interfaces
Section titled “Types and Interfaces”TypeScript’s type system is fully supported with TabScript syntax.
# Interface with propertiesinterface User name: string email: string age: number
# Interface with methodsinterface Service start||: void stop||: void getData|id: string|: Data
# Type aliasestype ID = string or numbertype Point = {x: number, y: number}
# Generic type with uniontype Result<T> = {success: true, data: T} or {success: false, error: string}
# Function types use pipestype Handler = |event: Event|: voidtype Mapper<T, U> = |input: T|: UEnums work the same as in TypeScript, with indentation instead of braces.
# Basic enumenum Color Red Green Blue
# Enum with explicit valuesenum Status Active = 1 Inactive = 0
# Traditional brace syntax also worksenum Direction { Up, Down, Left, Right }Plugins
Section titled “Plugins”TabScript’s plugin system lets you extend the language with custom syntax tailored to your domain. Plugins hook into the parser to recognize new syntax patterns and emit custom output while maintaining full IDE support.
The transpiler is lexer-less and single-pass — it reads input and emits output simultaneously without building an AST. This makes it fast and simple but best suited for transformations that map cleanly to underlying TypeScript constructs.
Using Plugins
Section titled “Using Plugins”Import plugins using the import plugin statement:
export default function||;tabscript 2.0import plugin "./my-plugin.tab"You can pass options to plugins using an object literal:
export default function|parser, options, pluginOptions| console.log(pluginOptions)tabscript 2.0import plugin "./options-demo.tab" {function: "UI", debug: true}Plugins can be loaded at any point in the file and take effect immediately. They can be written in TabScript (.tab) or JavaScript (.js).
How Plugins Work
Section titled “How Plugins Work”Plugins receive the Parser instance and can directly modify its parse* methods. The parser uses methods like parseStatement, parseExpression, and parseType to process different parts of the syntax. Plugins can replace or wrap these methods to add new syntax.
Writing a Simple Plugin
Section titled “Writing a Simple Plugin”Here’s a plugin that adds an @log decorator for automatic function call logging:
tabscript 2.0
import type {Parser, State, Options} from "tabscript"
export default function|p: Parser, options: Options| IDENTIFIER := p.pattern.. /[a-zA-Z_$][0-9a-zA-Z_$]*/ "identifier"
# Keep reference to original parseStatement origParseStatement := p.parseStatement.bind(p)
# Replace parseStatement to handle @log before other statements p.parseStatement = |s: State| if !s.read.. '@log' return origParseStatement(s)
# Parse: @log name := |args| body name := s.must.. s.read.. IDENTIFIER s.must.. s.read.. ':' isLet := !!s.read.. ':' s.emit.. (isLet ? 'let ' : 'const ') + name
s.must.. s.read.. '='
# Wrap function with logging s.emit.. '=(' s.must.. p.parseFuncParams(s) s.emit.. '=>{console.log(' + JSON.stringify(name) + ',...arguments);return(' s.must.. p.parseExpression(s) s.emit.. ');})'
return trueUsage:
tabscript 2.0import plugin "./log-plugin.tab"
@log add := |a: number, b: number| a + b
result := add(1, 2) # Logs: "add" 1 2Plugin API
Section titled “Plugin API”Plugins export a default function that receives three arguments:
parser- The Parser instance with allparse*methodsoptions- Global transpiler options (includesjsflag)pluginOptions- Options passed in the object literal after the plugin path
Modifying Parser Methods
Section titled “Modifying Parser Methods”To add custom syntax, save a reference to the original method and replace it with your own:
# Save original methodorigParseStatement := p.parseStatement.bind(p)
# Replace with custom implementationp.parseStatement = |s: State| if s.read.. '@custom' # Handle custom syntax return true # Fall back to original return origParseStatement(s)Common patterns:
- Run before original: Check for your syntax first, fall back to original if not matched
- Run after original: Call original first, try your syntax if it returns false
- Wrap original: Add behavior before/after calling the original
Token Matchers
Section titled “Token Matchers”Use p.pattern(regex, name) to create regex patterns for token matching. It automatically adds the sticky (/y) flag and provides descriptive error messages:
IDENTIFIER := p.pattern.. /[a-zA-Z_$][0-9a-zA-Z_$]*/ "identifier"NUMBER := p.pattern.. /[0-9]+/ "number"TAG := p.pattern.. /[a-z][a-z0-9-]*/ "tag-name"When a token fails to match, error messages will show the descriptive name (e.g., “expected
State API
Section titled “State API”The State object (s) provides methods for reading input and emitting output:
Reading Input:
s.read(pattern...)- Consume tokens, returns undefined if no matchs.peek(pattern...)- Look ahead without consumings.accept(pattern...)- Read and emit tokenss.must(value)- Throw error if value is falsy
Emitting Output:
s.emit(text...)- Add text to output
State Management:
s.snapshot()- Create checkpoint that can be revertedsnapshot.revert()- Revert input and output to checkpointsnapshot.revertOutput()- Revert only outputs.parseGroup(opts, func)- Parse delimited groups
Position Info:
s.inLine- Current input line numbers.hasMore()- Check if more input remains
Example: Markup DSL Plugin
Section titled “Example: Markup DSL Plugin”A more complex plugin can add entirely new syntax. Here’s a simplified markup plugin that transforms :div.class "text" into function calls:
tabscript 2.0
import type {Parser, State, Options} from "tabscript"
export default function|p: Parser, options: Options| funcName := "UI" TAG := p.pattern.. /[a-zA-Z][a-zA-Z0-9-]*/ "tag-name"
# Save original parseStatement origParseStatement := p.parseStatement.bind(p)
p.parseStatement = |s: State| if !s.read.. ':' return origParseStatement(s)
s.emit.. funcName + '(`'
# Parse tag name s.accept.. TAG
# Parse classes (.class) while s.read.. '.' s.emit.. '.' s.must.. s.accept.. TAG
s.emit.. '`'
# Parse text content snap := s.snapshot() s.emit.. ',' if !p.parseExpression(s) snap.revertOutput()
s.emit.. ');' return trueUsage:
tabscript 2.0import plugin "./markup.tab"
:div.container.highlight "Hello world"# Transpiles to: UI(`div.container.highlight`, "Hello world");Plugin Tips
Section titled “Plugin Tips”-
Return false on no match - Your plugin should return
falseif it doesn’t recognize the syntax, allowing other plugins or the default parser to try. -
Use snapshots for backtracking - If you start parsing and realize the syntax doesn’t match, use
snapshot().revert()to undo changes. -
Check
options.js- If you emit type annotations, checkoptions.jsand skip them when outputting JavaScript. -
Test thoroughly - Create
.taband.tstest files to verify your plugin output matches expectations. -
Bind original methods - When saving a reference to an original method, use
.bind(p)to preserve the correctthiscontext.
Try It Yourself
Section titled “Try It Yourself”All the code examples on this page are interactive! Click the “Edit” button on any example to modify the code and see the transpiled output update in real-time. Use the checkbox to toggle between TypeScript and JavaScript output.
CLI Options
Section titled “CLI Options”tabscript <input.tab> [options]
Options: --output <file> Output file --js Transpile to JavaScript --whitespace <mode> preserve (default) or pretty --debug Show debug output --recover Attempt to recover from errorsLearn More
Section titled “Learn More”- Check out comprehensive examples in tests/test.tab
- See plugin examples in tests/log-plugin.tab and tests/markup-plugin.tab
- Visit the GitHub repository for source code