npm/ kopytko-formatter v1.1.15

BrightScript Formatter

A hybrid 27-pass formatter for BrightScript with 49 configuration options. All options default to preserve so you can adopt it incrementally without changing existing style.

Install & setup

npm install --save-dev kopytko-formatter

Add scripts to package.json:

{
  "scripts": {
    "format": "kopytko-format --write \"src/**/*.brs\"",
    "format:check": "kopytko-format --check \"src/**/*.brs\""
  }
}

For VS Code format-on-save:

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "[brightscript]": {
    "editor.defaultFormatter": "bchelkowski.kopytko"
  }
}

Example config

kopytko-formatter.json
{
  "indentSize": 4,
  "endKeywordStyle": "spaced",
  "thenStyle": "always",
  "spaceAroundOperators": true,
  "spaceAroundAssignment": true,
  "commentStyle": "'",
  "spaceAfterCommentMarker": true,
  "sortImports": true,
  "emptyLineAfterImports": true,
  "emptyLinesBetweenFunctions": 1,
  "maxEmptyLines": 2,
  "trailingComma": "multiline",
  "associativeArrayBracketSpacing": true,
  "trimTrailingWhitespace": true,
  "insertFinalNewline": true
}

Config resolution: --config flagkopytko-formatter.json.vscode/settings.json (kopytko.format.*)

Live playground

Paste any BrightScript code and experiment with the options below — the output updates instantly. Try toggling End keywords, "then", and m. style to see the difference.

Formatter PlaygroundEdit code or config JSON — output updates live
Presets:
Input
Formatted output
' @import /utils/z-helper.brs
' @import /utils/a-helper.brs
sub processData(data,count,name)
    if count <> 0 then
        x = data.value + count
        m["total"] = x
        print "processing "+name
    endif
    for i = 0 to count-1
        if data.items[i] <> invalid
            print data.items[i]
        endif
    endfor
end sub
Config (JSONC) — all options with valid values in comments, edit any value

Before / after example

Unformatted BrightScript → formatted with the recommended config above.

Input
' Unformatted input
function  add(a ,b)
x=a+b
return x
end function
Formatted
function add(a, b)
    x = a + b
    return x
end function

Casing

10 independent casing dimensions with per-identifier overrides. Values: preserve, upper-case, lower-case, capitalize, pascal-case, camel-case.

.vscode/settings.json
// .vscode/settings.json
{
  "kopytko.casing.keyword": "lower-case",
  "kopytko.casing.builtin": "pascal-case",
  "kopytko.casing.type": "pascal-case",
  "kopytko.casing.literal": "lower-case",
  "kopytko.casing.logicOperator": "lower-case",
  "kopytko.casing.userFunction": "camel-case",
  "kopytko.casing.exact": {
    "createObject": "pascal-case"
  }
}
Input (mixed case)
FUNCTION add(A AS INTEGER, B AS INTEGER) AS INTEGER
  RETURN A + B
END FUNCTION
With casing config
function add(a as Integer, b as Integer) as Integer
  return a + b
end function
Setting Controls Default
kopytko.casing.builtin Built-in functions (StrToI, Left, Right…) preserve
kopytko.casing.keyword Keywords (if, for, function…) — fallback for sub-categories preserve
kopytko.casing.type Type names (Integer, String, Object…) falls back to keyword
kopytko.casing.literal true, false, invalid falls back to keyword
kopytko.casing.logicOperator and, or, not falls back to keyword
kopytko.casing.mathOperator mod falls back to keyword
kopytko.casing.method ro* component methods preserve
kopytko.casing.userFunction User-defined functions preserve
kopytko.casing.userMethod User-defined AA methods preserve
kopytko.casing.exact Per-identifier map of overrides {}

All options

49 formatting options, all defaulting to 'preserve'. Click any option's anchor to link directly.

Indentation & whitespace

indentSize number default: 4

Number of spaces per indent level when useTabs is false.

indentSize: 2
function main()
  if true then
    print "nested"
  end if
end function
indentSize: 4
function main()
    if true then
        print "nested"
    end if
end function
useTabs boolean default: false

Use tab characters instead of spaces for indentation.

useTabs: false
function main()
  print "spaces"
end function
useTabs: true
function main()
	print "tabs"
end function
maxEmptyLines number default: 2

Maximum number of consecutive blank lines allowed. 0 = no limit.

maxEmptyLines: 2 (input)
function a()
end function



function b()
end function
maxEmptyLines: 1 (output)
function a()
end function

function b()
end function
emptyLinesBetweenFunctions number default: 1

Exact number of blank lines between top-level function/sub declarations.

0 blank lines (input)
function a()
end function
function b()
end function
1 blank line (output)
function a()
end function

function b()
end function
emptyLinesBetweenMethods number default: 1

Exact number of blank lines between AA method definitions inside a builder function.

trimTrailingWhitespace boolean default: true

Remove all trailing whitespace from the end of each line.

insertFinalNewline boolean default: true

Ensure the file ends with exactly one newline character.

lineEnding 'lf' | 'crlf' | 'auto' default: 'auto'

Line ending style. 'auto' preserves the file's existing line endings.

Compound keywords

endKeywordStyle 'spaced' | 'compact' | 'preserve' default: 'preserve'

Controls whether compound end keywords use a space ('end if') or are compact ('endif').

compact
if x = 1 then
  print "one"
endif

while x > 0
  x--
endwhile
spaced
if x = 1 then
  print "one"
end if

while x > 0
  x--
end while
thenStyle 'always' | 'never' | 'multiline-only' | 'singleline-only' | 'preserve' default: 'preserve'

Controls whether 'then' appears on if conditions.

thenStyle: preserve (input)
if x = 1
  print "one"
end if

if y > 0 then print "pos"
thenStyle: 'always'
if x = 1 then
  print "one"
end if

if y > 0 then print "pos"

Functions & subs

functionVsSubForVoid 'function' | 'sub' | 'allow-void' | 'preserve' default: 'preserve'

Enforce consistent use of function vs sub for procedures that return nothing.

preserve
function doWork() as Void
  print "work"
end function
sub
sub doWork()
  print "work"
end sub
spaceBeforeNamedFunctionParens boolean default: false

Insert a space between the function name and its opening parenthesis in definitions.

false
function add(a, b)
  return a + b
end function
true
function add (a, b)
  return a + b
end function
spaceBeforeAnonymousFunctionParens boolean default: false

Insert a space between the function keyword and its opening parenthesis in anonymous function expressions.

false
callback = function(x)
  return x
end function
true
callback = function (x)
  return x
end function
spaceBeforeCallParens boolean default: false

Insert a space between function name and '(' in call expressions.

false
result = add(1, 2)
print result
true
result = add (1, 2)
print result
spaceInsideParens 'never' | 'always' default: 'never'

Enforce spaces inside parentheses in calls and definitions.

'never'
result = add(1, 2)
'always'
result = add( 1, 2 )
paramAlignmentStyle 'indent' | 'align-to-paren' | 'preserve' default: 'preserve'

How to align parameters when a function signature spans multiple lines. Note: in BrightScript, multi-line parameter lists require _ line-continuation characters. This pass is not yet implemented — the option is accepted but has no effect on formatting output.

Operators & expressions

spaceAroundOperators boolean default: true

Enforce spaces around binary operators: +, -, *, /, \, ^, <, >, <=, >=, <>, and, or, mod.

false
x=a+b*c
valid=x>0 and x<100
true
x = a + b * c
valid = x > 0 and x < 100
spaceAroundAssignment boolean default: true

Enforce spaces around = in standalone assignment statements.

false
x=1
name="hello"
true
x = 1
name = "hello"
unarySpacing boolean default: true

Enforce a space after the unary 'not' operator.

false (no space)
result = not valid
true (space enforced)
result = not valid

Comments

commentStyle "'" | 'rem' | 'preserve' default: 'preserve'

Normalize all comment markers to tick (') or rem.

preserve (mixed input)
rem initialize the screen
x = 1
' set the value
y = 2
commentStyle: "'"
' initialize the screen
x = 1
' set the value
y = 2
spaceAfterCommentMarker boolean default: true

Enforce a single space between the comment marker (') and the comment text.

false
'No space after tick
x = 1  'Also no space
true
' Space after tick
x = 1  ' Also space
commentWidth number default: 0

Maximum line length for comment lines. 0 = no limit.

Imports

sortImports boolean default: false

Sort @import statements alphabetically (module-relative imports first, then absolute paths).

unsorted
' @import /utils/z-helper.brs
' @import /utils/a-helper.brs
' @import /components/Widget.brs
sortImports: true
' @import /components/Widget.brs
' @import /utils/a-helper.brs
' @import /utils/z-helper.brs
emptyLineAfterImports boolean default: false

Insert a blank line after the last @import line and before the first line of code.

false
' @import /utils/helper.brs
function main()
  print "hello"
end function
true
' @import /utils/helper.brs

function main()
  print "hello"
end function

Blank lines

emptyLineAfterFunctionOpen boolean default: false

Insert a blank line after the function/sub declaration line.

false
function main()
  x = 1
end function
true
function main()

  x = 1
end function
emptyLineBeforeFunctionClose boolean default: false

Insert a blank line before end function/sub.

false
function main()
  print "done"
end function
true
function main()
  print "done"

end function
emptyLineBeforeReturn 'always' | 'not-alone' | false default: false

Insert a blank line before return statements. 'not-alone' skips when return is the only statement.

false
function getValue()
  x = compute()
  return x
end function
'always'
function getValue()
  x = compute()

  return x
end function
emptyLineBeforeComment boolean default: false

Enforce a blank line before a stand-alone comment line.

false
x = 1
' compute result
y = x + 1
true
x = 1

' compute result
y = x + 1

Control flow

parenthesisIfCase 'always' | 'never' | 'preserve' default: 'preserve'

Control whether if conditions are wrapped in parentheses. Works on any condition expression including method calls and comparisons (e.g. 'if a.someMethod() > 0' → 'if (a.someMethod() > 0)').

preserve / never
if x = 1 then
  print "one"
end if

if result > threshold then
  print "ok"
end if
'always'
if (x = 1) then
  print "one"
end if

if (result > threshold) then
  print "ok"
end if
elseOnNewLine boolean default: true

Intended to control else placement. This pass is not yet implemented — the option is accepted but has no effect on formatting output.

forLoopSpacing boolean default: true

Enforce spaces around 'to' and 'step' keywords in for-loop headers.

false
for i=1 to 10 step 2
  print i
end for
true
for i = 1 to 10 step 2
  print i
end for

Arrays & associative arrays

associativeArrayBracketSpacing boolean default: true

Spaces inside {} braces for inline associative arrays: { key: value } vs {key: value}.

false
data = {name: "Alice", age: 30}
true
data = { name: "Alice", age: 30 }
trailingComma 'never' | 'always' | 'multiline' default: 'never'

Trailing comma after the last element in arrays/AA literals.

'never'
items = [
  "apple",
  "banana",
  "cherry"
]
'multiline'
items = [
  "apple",
  "banana",
  "cherry",
]
arrayCommaStyle 'always' | 'never' | 'preserve' default: 'preserve'

Comma separators between entries in multi-line arrays. BrightScript allows omitting commas when entries are on separate lines.

'never'
items = [
  "apple"
  "banana"
]
'always'
items = [
  "apple",
  "banana",
]
associativeArrayCommaStyle 'always' | 'never' | 'preserve' default: 'preserve'

Comma separators between entries in multi-line associative arrays. BrightScript allows omitting commas when entries are on separate lines.

'never'
data = {
  name: "Alice"
  age: 30
}
'always'
data = {
  name: "Alice",
  age: 30,
}
associativeArraySingleLineThreshold number default: 0

Force multi-line layout when an AA has more than this many keys. 0 = no threshold.

threshold: 0 (preserve)
data = { a: 1, b: 2, c: 3, d: 4 }
threshold: 3 (4 keys → multiline)
data = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
}
arraySplitOpenBracket boolean default: false

Split [{ onto separate lines for multi-item array-of-objects.

false (preserve)
items = [{ id: 1, name: "a" }, { id: 2, name: "b" }]
true
items = [
  { id: 1, name: "a" },
  { id: 2, name: "b" },
]

BrightScript patterns

mPrefixStyle 'dot' | 'bracket' | 'preserve' default: 'preserve'

Normalize m-scope field access to dot notation or bracket notation.

bracket (input)
m["count"] = m["count"] + 1
m["name"] = "test"
mPrefixStyle: 'dot'
m.count = m.count + 1
m.name = "test"
alignAssignments boolean default: false

Align = signs in consecutive assignment statements.

false
firstName = "John"
lastName = "Doe"
age = 30
isActive = true
true
firstName = "John"
lastName  = "Doe"
age       = 30
isActive  = true
observeFieldStyle 'always-scoped' | 'preserve' default: 'preserve'

Enforce observeFieldScoped() over observeField() for task/render-safe observation. Using non-scoped observeField() can cause memory leaks in long-lived Task nodes.

preserve (input)
m.top.observeField("myField", "onChanged")
'always-scoped'
m.top.observeFieldScoped("myField", "onChanged")
fieldAccessConsistency 'dot' | 'method' | 'preserve' default: 'preserve'

Normalize SceneGraph node field access between dot notation and getField()/setField() methods.

method (input)
value = m.top.getField("myField")
m.top.setField("result", 42)
dot
value = m.top.myField
m.top.result = 42

Miscellaneous

printStatement 'warn' | 'remove' | 'preserve' default: 'preserve'

Flag or remove 'print' debug statements.

preserve (input)
function main()
  print "debug: x = " + x.toStr()
  result = compute()
  print "done"
  return result
end function
'remove'
function main()
  result = compute()
  return result
end function
lineCommentPosition 'above' | 'inline' | 'preserve' default: 'preserve'

Move trailing line comments. 'above' puts them on the line above the code they annotate.

'preserve'
x = 1 ' the initial value
'above'
' the initial value
x = 1
maxLineLength number default: 120

Maximum line length. Lines exceeding this are flagged (works with wrapLongStrings). 0 = no limit.

wrapLongStrings 'plus' | 'array-join' | 'preserve' default: 'preserve'

How to wrap string literals that exceed maxLineLength.

preserve
message = "This is a very long string that will definitely exceed the maximum configured line length limit"
'plus'
message = "This is a very long string that will definitely " + _
        "exceed the maximum configured line length limit"
stringConcatStyle 'preserve' | 'plus' | 'array-join' default: 'preserve'

Style used when wrapping long strings that need concatenation: '+' operators or an array-join expression.

'plus'
message = "This is a very long string that will definitely " + _
        "exceed the maximum configured line length limit"
'array-join'
message = ["This is a very long string that will definitely ", _
        "exceed the maximum configured line length limit"].join("")
emptyLinesAtBlockBoundaries 'strip' | 'enforce' | 'preserve' default: 'preserve'

Control blank lines at the start and end of blocks (if, for, while bodies).

verifySyntax boolean default: true

Re-parse the formatted output and abort the write if it fails to parse, to guard against formatter bugs corrupting source files.

Library API

import { formatText, checkFormatting } from 'kopytko-formatter';

// Format a BrightScript source string
const formatted = formatText(source, {
  indentSize: 4,
  endKeywordStyle: 'spaced',
  spaceAroundOperators: true,
});

// Check if source is already formatted (returns boolean, no mutation)
const isClean = checkFormatting(source, config);
if (!isClean) {
  console.log('File needs formatting');
}