# Script Language Syntax

This document describes the script language implemented in Paragrapher as it currently exists in code.

It is a small, case-sensitive language with BASIC-like statements and block/document objects.

## Document Structure

The document is a linear sequence of **blocks**. Each block represents a piece of content in the manuscript.

### Block Types

- **TEXT** — Prose/fiction content (the main story text)
- **SUMMARY** — Summary of upcoming story events (for continuity)
- **LORE** — World-building notes (characters, settings, rules)
- **OUTLINE** — Chapter/outline notes

### Block Properties

Every block has:

- **text** — the current draft content (string)
- **type** — one of TEXT, SUMMARY, LORE, OUTLINE (constant)
- **label** — an optional short label (e.g., "Chapter 1") stored as `block.label`
- **variations** — a collection of alternative drafts for the same content
- **variationCount** — how many variations exist
- **liveIndex** — which variation is currently selected (1-based, or 0 for latest)

### Variations

Each block can have multiple **variations** — different versions of the same content. The variation shown in the reader is the **live variation**. You can:

- Add new variations (`block.addVariation(text)`)
- Switch which is live (`block.setLive(index)`)
- Get a specific variation (`block.getVariation(index)`)

The `block.text` property always returns the live variation's content.

### Iterating Blocks

Use `for block in document` to loop through all blocks in order. The blocks collection is `document.blocks` and is zero-indexed.

Strings can be written in two forms:

- normal quoted strings: `"Hello\nWorld"`
- triple-quoted multiline strings:

```txt
let prompt = """
You are a fiction writer.
Write vivid prose.
Return only the prose.
"""
```

## Case Sensitivity

The language is case-sensitive.

Examples:

- `for` is a keyword
- `FOR` is not
- `TEXT`, `SUMMARY`, `LORE`, `OUTLINE` are built-in constants
- `text`, `summary`, `lore` are not the same thing
- `document` is a built-in global
- `DOCUMENT` is not

## Comments

Line comments start with `//`.

Example:

```txt
// this is a comment
let x = 10
```

## Statements

Supported statements:

- `let`
- assignment
- `print`
- `stop`
- `break`
- `continue`
- `return`
- `global`
- `for ... in`
- `for ... = ... to ... [step ...]`
- `while ... wend`
- `if ... then ... [else ...] endif`
- `sub ... end sub`


## Declaration Rules

Variables must be declared before use.

- Use `let` to declare a variable for the first time.
- Use `=` only to update an existing variable.
- Subroutine parameters are automatically declared.
- A `for` loop variable is declared by the `for` statement itself.
- `global x` makes a top-level variable available inside a subroutine.
- Assigning to an undeclared identifier is a runtime error.

### `let`

Declares a new variable in the current scope.

```txt
let name = "value"
let count = 0
let arr = []
let nums = [1, 2, 3]
```

### Assignment

Assigns to an existing variable or writable target.
Assignment does not declare variables. If the name does not already exist in the current scope, the script raises an error. Use `let` for first declaration.

```txt
name = "new value"
count = count + 1
block.text = "Updated"
block.label = "Chapter"
block.variations[0].text = "New text"
```

Array literals are supported:

- `[]` creates an empty array
- `[1, 2, 3]` creates an array with three items
- `["a", "b"]` creates a string array

### `print`

```txt
print "Hello"
print block.text
```

### `stop`

Stops the script immediately without reporting an error.

```txt
if document.selectedBlock == null then
    print "No block selected."
    stop
endif
```

### `break`

Exits the nearest enclosing `for` or `while` loop immediately.

```txt
for block in document
    if block.type == SUMMARY then
        break
    endif
next block
```

Using `break` outside a loop is a runtime error.

### `continue`

Skips the rest of the current iteration of the nearest enclosing `for` or `while` loop and proceeds to the next iteration.

```txt
for block in document
    if block.type <> TEXT then
        continue
    endif
    print block.text
next block
```

Using `continue` outside a loop is a runtime error.

### `return`

Returns a value from the current subroutine immediately.

```txt
sub chapterTitle(text)
    let linesArr = lines(text)
    if len(linesArr) == 0 then
        return "Untitled Chapter"
    endif
    return trim(linesArr[0])
end sub
```

Using `return` outside a subroutine is a runtime error.

### `sub ... end sub`

Defines a top-level subroutine. Subroutines are called like normal functions.

```txt
sub chapterTitle(text)
    let linesArr = lines(text)
    if len(linesArr) == 0 then
        return "Untitled Chapter"
    endif
    return trim(linesArr[0])
end sub

let title = chapterTitle(block.text)
```

Rules:

- subroutines must be declared at the top level
- nested subroutines are not supported
- subroutines run in their own isolated local scope — see [Variable Scope](#variable-scope)
- if a subroutine reaches `end sub` without `return`, it returns `null`

### `global`

Declares that a variable name inside a subroutine refers to a top-level script variable. Must be used inside a subroutine body. Has no effect at the top level (runtime error).

```txt
let total = 0

sub addWords(n)
    global total
    total = total + n
end sub

addWords(10)
addWords(5)
print total   // 15
```

- global `total` makes both reads and writes of `total` inside the subroutine operate on the top-level variable.
- Without `global x`, reading `x` inside the subroutine is an **undefined-variable error**.
- A single `global` statement declares one name. Repeat for each name needed:

```txt
sub process()
    global count
    global results
    count = count + 1
    results = append(results, "done")
end sub
```

## Variable Scope

The language uses a three-layer scope model.

### Layer 1 — Built-in scope

Always visible everywhere, including inside subroutines. Contains:

- Standard objects: `document`, `llm`, `promptFlow`, `input`
- Type constants: `TEXT`, `SUMMARY`, `LORE`, `OUTLINE`

You never need `global` to access these.

### Layer 2 — Top-level (script) scope

Variables declared with let at the top level of the script live here.

```txt
let title = "My Story"   // top-level variable
```

These are **not** visible inside subroutines unless explicitly imported with `global`.

### Layer 3 — Subroutine local scope

Each subroutine call gets its own isolated local scope.

- Parameters are local and already declared.
- Other new local variables must be declared with `let`.
- A `for` loop variable is declared by the `for` statement itself.
- Bare assignment updates an existing local variable, parameter, loop variable, or a name declared with `global`.
- Top-level variables are not visible inside a subroutine unless explicitly imported with `global`.

```txt
let count = 0

sub test(n)
    let localCount = n
    localCount = localCount + 1

    global count
    count = count + localCount
end sub
```

### Explicit global access

Use `global x` inside a subroutine to make `x` a read/write alias for the top-level variable of the same name:

```txt
let count = 0

sub increment()
    global count
    count = count + 1
end sub
```

Without `global count`, reading `count` inside the subroutine produces a runtime error:

```
Error at line N: Undefined variable: count
```

### Reading a global into a local copy

If you only need to read a top-level value and want a local working copy, declare it global, copy it, then work locally:

```txt
let basePrompt = "Rewrite this."

sub buildPrompt(extra)
    global basePrompt
    let prompt = basePrompt & " " & extra   // local copy
    return prompt
end sub
```

### `global` placement

`global` can appear anywhere inside the subroutine body, including inside loops or branches, but the convention is to place all `global` declarations at the top of the subroutine for clarity.

### `for ... in`

A `for` loop variable is declared automatically by the loop statement.

```txt
for block in document
    print block.text
next block
```

You can also iterate arrays and collections exposed by script objects, for example:

```txt
for v in block.variations
    print v.text
next v
```

```txt
for part in split("a,b,c", ",")
    print part
next part
```

### `for ... = ... to ... [step ...]`

A `for` loop variable is declared automatically by the loop statement.

```txt
for i = 1 to 10
    print i
next i
```

```txt
for i = 10 to 1 step -1
    print i
next i
```

### `while`

```txt
let i = 0
while i < 10
    print i
    i = i + 1
wend
```

### `if`

```txt
if block.type == SUMMARY then
    print "summary block"
else
    print "not summary"
endif
```

## Expressions

Supported expression features:

- numbers
- strings
- booleans: `true`, `false`
- identifiers
- function calls
- member access
- method calls
- index access
- parentheses

### Literals

Numbers:

```txt
1
3.14
```

Strings:

```txt
"hello"
"line\nbreak"
```

Supported escapes in strings:

- `\"`
- `\\`
- `\n`
- `\t`
- `\r`

Booleans:

```txt
true
false
```

Null:

```txt
null
```

### Operators

Arithmetic:

- `+`
- `-`
- `*`
- `/`

String concatenation:

- `+` (preferred — works for numbers and strings alike)
- `&` (also supported, BASIC-style)

Comparison:

- `==` (equality)
- `<>` or `!=` (not equal)
- `<`
- `<=`
- `>`
- `>=`

Note:

- `=` is assignment, not equality comparison
- using `=` inside an expression is a parse error; use `==` instead

Logical:

- `and`
- `or`
- `not`

### Operator Precedence

Highest to lowest:

1. postfix access/calls/indexing
2. unary `-`, `not`
3. `*`, `/`
4. `+`, `-`, `&`
5. comparisons
6. `and`
7. `or`

## Postfix Syntax

### Member access

```txt
block.text
block.label
document.blocks
```

### Method call

```txt
block.addVariation("new rewrite")
document.insertBlockAfter(block, SUMMARY, "summary text")
llm.call("prompt")
```

### Index access

```txt
document.blocks[0]
block.variations[1]
```

## Built-in Globals

### `document`

Built-in global object representing the current document.

### `llm`

Built-in global object for LLM calls.

### `input`

Built-in global object for runtime script forms.

Call `input.begin(title)` first, then add controls, then call `input.show()`.

Example:

```txt
input.begin("Rewrite Block")
input.addText("name", "Chapter name", "Chapter")
input.addMultiLine("prompt", "Prompt", "Tighten this scene.", 5)
input.addSelector("mode", "Mode", "overwrite;new variation", "new variation")
input.addCheckbox("include_lore", "Include lore", true)

if not input.show() then
    stop
endif

print input.getText("name")
print input.getText("mode")
print str(input.getBool("include_lore"))
```

Properties (read/write):

- `llm.model` — model name; uses the app default if empty
- `llm.temperature`
- `llm.systemPrompt`

Methods:

- `llm.addUser(text)` — append a user message
- `llm.addAssistant(text)` — append an assistant message
- `llm.clearMessages()` — clear the message list (keeps systemPrompt/model/temperature)
- `llm.reset()` — clear messages and reset all properties to defaults
- `llm.call()` — execute with the current message list
- `llm.call(prompt)` — append `prompt` as a user message then execute
- `llm.callBudgeted(prompt, contextWindowTokens, reserveOutputTokens)` — append `prompt` as a user message, trim oldest prior messages as needed to fit the estimated input budget, reserve output tokens via `max_tokens`, then execute
- `llm.summarize(block, includeLore?)` — build and execute the full summary request for the given summary block using the app's prompt-flow settings; returns the result text
- `llm.applyLive(block, instruction, addNew?)` — read the block's active variation text, send `instruction + "\n\n" + text` to the LLM, then write the result back; `addNew=false` (default) overwrites the active variation in-place, `addNew=true` adds the result as a new variation; returns `true` on success

`llm.call(...)`, `llm.callBudgeted(...)`, `llm.summarize(...)`, and `llm.applyLive(...)` execute non-streaming requests using the app's configured base URL, API key, and include-model setting.

`llm.callBudgeted(...)` keeps the system prompt and newest message, trims oldest prior messages first, and logs what it trimmed. `llm.summarize` and `llm.applyLive` use `llm.model` if set, otherwise the app default model. `llm.applyLive` uses `llm.systemPrompt` if set, otherwise the app's default system prompt. `includeLore` defaults to `true`.

Example:

```txt
llm.reset()
llm.systemPrompt = "You are a concise editor."
llm.addUser("Keep the tone but tighten the prose.")
let result = llm.call(block.text)
```

### `promptFlow`

Built-in global object that exposes the app's current prompt-flow settings and the same prompt-building helpers used by the agent runner.

Properties (read-only):

- `promptFlow.systemPrompt`
- `promptFlow.summarizerSystemPrompt`
- `promptFlow.summariesBridgePrompt`
- `promptFlow.previousParagraphsBridgePrompt`

- `promptFlow.rewritePrompt`
- `promptFlow.summaryPrompt`

Methods:

- `promptFlow.buildSystemPrompt(loreText)` — builds the full rewrite system prompt: `systemPrompt` + lore block (if non-empty)
- `promptFlow.buildSummarizerSystemPrompt(loreText)` — same but uses `summarizerSystemPrompt`
- `promptFlow.buildSummariesAssistantBlock(summariesText)` — returns the summaries text as-is (no wrapping)
- `promptFlow.buildPreviousParagraphsAssistantBlock(text)` — returns the text as-is (no wrapping)
- `promptFlow.buildFinalUserPrompt(promptText, sourceText)` — substitutes `sourceText` into `promptText` (replaces `{current_variation}`)
- `promptFlow.buildSummaryFinalUserPrompt(promptText, sourceText, summaryRangeText)` — same but also substitutes `{text_summary_range}`
- `promptFlow.wrapTag(tag, content)` — wraps `content` in `<tag>…</tag>`

## Built-in Constants

These are highlighted as constants:

- `TEXT`
- `SUMMARY`
- `LORE`
- `OUTLINE`

Runtime values:

- `TEXT` -> `"Text"`
- `SUMMARY` -> `"Summary"`
- `LORE` -> `"Lore"`
- `OUTLINE` -> `"Outline"`

These are intended for comparisons such as:

```txt
if block.type == SUMMARY then
    print block.text
endif
```

## Built-in Functions

These are global functions/builtins currently registered:

- `replace(s, from, to)`
- `replace_nocase(s, from, to)`
- `remove(s, sub)`
- `split(s, delim)`
- `join(arr, delim)`
- `removeEmpty(arr)`
- `append(arr, value)`
- `removeAt(arr, index)`
- `insertAt(arr, index, value)`
- `repeat(s, n)`
- `trim(s)`
- `ltrim(s)`
- `rtrim(s)`
- `len(x)`
- `upper(s)`
- `lower(s)`
- `titleCase(s)`
- `padLeft(s, n, ch)`
- `padRight(s, n, ch)`
- `contains(s, sub)`
- `indexOf(s, sub)`
- `lastIndexOf(s, sub)`
- `count(s, sub)`
- `contains_nocase(s, sub)`
- `startsWith(s, prefix)`
- `starts_with_nocase(s, prefix)`
- `endsWith(s, suffix)`
- `ends_with_nocase(s, suffix)`
- `before(s, sub)`
- `after(s, sub)`
- `beforeLast(s, sub)`
- `afterLast(s, sub)`
- `left(s, n)`
- `right(s, n)`
- `mid(s, start, len)`
- `lines(s)`
- `between(s, openTag, closeTag)`
- `str(x)`
- `int(x)`
- `print(x)`
- `wordCount(s)`
- `sentenceCount(s)`
- `tokenEstimate(s)`
- `abs(n)`
- `max(a, b)`
- `min(a, b)`

### Function Details

#### `replace(s, from, to)`

Replaces all occurrences of `from` in `s`.

```txt
print replace("abc abc", "abc", "x")
```

#### `replace_nocase(s, from, to)`

Replaces all occurrences of `from` in `s`, case-insensitively.

```txt
print replace_nocase("Teh teh TEH", "teh", "the")
```

#### `remove(s, sub)`

Removes all occurrences of `sub` from `s`.

#### `split(s, delim)`

Returns an array.

```txt
let parts = split("a,b,c", ",")
print parts[0]
```

If `delim` is empty, splits into characters.

#### `join(arr, delim)`

Joins array items into a string.

```txt
print join(split("a,b,c", ","), "-")
```

#### `removeEmpty(arr)`

Returns a new array with empty-string items removed.

#### `append(arr, value)`

Returns a new array with `value` appended to the end.

```txt
let arr = []
arr = append(arr, "x")
arr = append(arr, "y")
print len(arr)
```

#### `removeAt(arr, index)`

Returns a new array with the item at `index` removed.

If `index` is out of range, the array is returned unchanged.

```txt
let arr = split("a,b,c", ",")
arr = removeAt(arr, 1)
print join(arr, ",")
```

#### `insertAt(arr, index, value)`

Returns a new array with `value` inserted at `index`.

`index` is clamped into the valid range:

- less than `0` inserts at the beginning
- greater than the array length inserts at the end

```txt
let arr = split("a,c", ",")
arr = insertAt(arr, 1, "b")
print join(arr, ",")
```

#### `repeat(s, n)`

Repeats a string `n` times.

```txt
print repeat("-", 10)
```

#### `trim(s)`

Trims leading and trailing whitespace.

#### `ltrim(s)`

Trims leading whitespace only.

#### `rtrim(s)`

Trims trailing whitespace only.

#### `len(x)`

If `x` is an array, returns array length.
Otherwise returns string length.

#### `upper(s)`

Uppercases a string.

#### `lower(s)`

Lowercases a string.

#### `titleCase(s)`

Converts a string to title case.

Useful for labels and headings.

#### `padLeft(s, n, ch)`

Pads `s` on the left up to width `n` using the first character of `ch`.

#### `padRight(s, n, ch)`

Pads `s` on the right up to width `n` using the first character of `ch`.

#### `contains(s, sub)`

Returns boolean.

#### `indexOf(s, sub)` / `indexOf(arr, value)`

For strings, returns the zero-based index of `sub` in `s`, or `-1` if not found.

For arrays, returns the zero-based index of the first item equal to `value`, or `-1` if not found.

#### `lastIndexOf(s, sub)`

Returns the zero-based index of the last occurrence of `sub` in `s`, or `-1` if not found.

#### `count(s, sub)`

Counts non-overlapping occurrences of `sub` in `s`.

#### `contains_nocase(s, sub)`

Returns boolean, case-insensitively.

#### `startsWith(s, prefix)`

Returns boolean.

#### `starts_with_nocase(s, prefix)`

Returns boolean, case-insensitively.

#### `endsWith(s, suffix)`

Returns boolean.

#### `ends_with_nocase(s, suffix)`

Returns boolean, case-insensitively.

#### `before(s, sub)`

Returns the part of `s` before the first occurrence of `sub`, or an empty string if not found.

#### `after(s, sub)`

Returns the part of `s` after the first occurrence of `sub`, or an empty string if not found.

#### `beforeLast(s, sub)`

Returns the part of `s` before the last occurrence of `sub`, or an empty string if not found.

#### `afterLast(s, sub)`

Returns the part of `s` after the last occurrence of `sub`, or an empty string if not found.

#### `left(s, n)`

Returns the leftmost `n` characters.

#### `right(s, n)`

Returns the rightmost `n` characters.

#### `mid(s, start, len)`

Substring using 1-based start index, BASIC-style.

```txt
print mid("abcdef", 2, 3)   // "bcd"
```

#### `lines(s)`

Splits a string into an array of lines.

#### `between(s, openTag, closeTag)`

Returns the content between the first occurrence of `openTag` and the first occurrence of `closeTag` that follows it. Returns an empty string if either tag is not found or if either tag argument is empty.

```txt
let response = """
<title>Midnight Audience</title>
<name>Captain Mira Vale</name>
<lore>Captain Mira Vale is controlled, intelligent, and distrustful.</lore>
"""

let title = between(response, "<title>", "</title>")
let name  = between(response, "<name>",  "</name>")
let lore  = between(response, "<lore>",  "</lore>")

print title   // Midnight Audience
print name    // Captain Mira Vale
print lore    // Captain Mira Vale is controlled, intelligent, and distrustful.
```

Notes:

- Matching is case-sensitive: `<Title>` and `<title>` are different tags.
- Only the first match is returned. To extract all occurrences use a loop with `after()` to advance past each one.
- Content is returned raw, without trimming. Use `trim()` to strip leading/trailing whitespace if the model added newlines inside the tags.

```txt
let title = trim(between(response, "<title>", "</title>"))
```

#### `str(x)`

Converts value to string.

#### `int(x)`

Converts value to integer.

#### `print(x)`

Builtin form returns the passed value.

The `print` statement is what actually logs script output.

#### `wordCount(s)`

Counts whitespace-separated words. Uses `iswspace` so tabs, newlines, and other Unicode whitespace all act as delimiters; runs of whitespace count as one gap.

#### `sentenceCount(s)`

Approximate sentence count.

It avoids some common false positives such as:

- `Mr.`
- `Dr.`
- `etc.`
- decimal numbers like `3.14`

#### `tokenEstimate(s)`

Estimates the BPE token count for English prose, approximating the cl100k tokenizer used by GPT-4 and similar models.

The heuristic currently works like this:

- each whitespace-separated word counts as one base token
- trailing punctuation such as `.`, `,`, `!`, `?`, `;`, `:` adds extra tokens
- common contractions such as `n't`, `'re`, `'ve`, `'ll`, `'d`, `'m` add an extra token
- possessives such as `'s` or a trailing apostrophe add an extra token
- hyphens add extra tokens
- long word parts with 10 or more letters add an extra token
- the final total is scaled slightly upward (`× 1.026`) for residual BPE overhead

Returns an integer. It is an approximation, not an exact tokenizer implementation.

```txt
let tokens = tokenEstimate(block.text)
print "~" + str(tokens) + " tokens"
```

#### `abs(n)`

Absolute value.

#### `max(a, b)`

Returns the larger value.

#### `min(a, b)`

Returns the smaller value.

## Object Model

## `document`

Properties:

- `document.blockCount`
- `document.blocks`
- `document.selectedBlock` — the currently selected block, or `null` if nothing is selected; writable: assigning a block object selects it and scrolls it into view

Example:

```txt
document.selectedBlock = block
```

Methods:

- `document.getSummaries(block, maxCount?)`
- `document.getLastSummary(block)`
- `document.collectSummaryText(block)`
- `document.getLore(block, maxCount?)`
- `document.getPreviousBlocksText(block, n)`
- `document.getBlockIndex(block)`
- `document.insertBlockAfter(block, type, text)`
- `document.deleteBlock(block)`
- `document.deleteBlocks(blockArray)`
- `document.deleteBlocksAfter(block)`
- `document.saveIncremental(postfix?, filename?, overwrite?)`
- `document.setLive(index)`
- `document.collapse()`
- `document.splitParagraphs()`
- `document.addSummaryPoints(prefix?)`

### Block argument

All document methods that accept a block argument require a **block object** — not a numeric ID or index.

Pass `null` to mean "the entire document" (no upper bound cutoff).

```txt
// correct — pass the block object from iteration
for block in document
    let summaries = document.getSummaries(block)
next block

// correct — look up by 0-based position index, then pass the object
let b = document.blocks[9]
let summaries = document.getSummaries(b)

// correct — null means the whole document
let summaries = document.getSummaries(null)

// WRONG — integers are not accepted; stops the script with a runtime error
let summaries = document.getSummaries(10)
```

### `document.getSummaries(block, maxCount?)`

Returns the joined summary text up to, but not including, the given block.

- `maxCount` — optional; if greater than zero, only the last N summaries are returned. If omitted or zero, all summaries are returned.

Pass `null` as the block to collect from the entire document (subject to `maxCount`).

This mirrors the summary context assembly used by the normal rewrite path.

### `document.getLastSummary(block)`

Returns only the nearest previous summary text before the given block, or an empty string if there is none.

Pass `null` to get the last summary in the entire document.

### `document.collectSummaryText(block)`

Returns the text range that would be summarized for the given summary block:

- after the previous summary point
- up to, but not including, the given summary block
- only text blocks are included

Pass `null` to collect all text blocks from the last summary point to the end of the document.

### `document.getLore(block, maxCount?)`

Returns the joined lore text up to, but not including, the given block.

- `maxCount` — optional; if greater than zero, only the last N lore blocks are returned. If omitted or zero, all lore blocks are returned.

Pass `null` as the block to collect from the entire document (subject to `maxCount`).

This mirrors the lore context assembly used by the normal rewrite path.

### `document.getPreviousBlocksText(block, n)`

Returns the text of the `n` text blocks immediately preceding the given block, joined by blank lines.

Used to supply recent prior prose to the LLM as style and continuity context, mirroring the `includePreviousBlocks` option in the agent runner.

`n` must be greater than zero; returns an empty string if there are no prior text blocks or `n` is 0.

```txt
let prev = document.getPreviousBlocksText(block, 3)
if len(prev) > 0 then
    llm.addUser(promptFlow.previousParagraphsBridgePrompt)
    llm.addAssistant(promptFlow.buildPreviousParagraphsAssistantBlock(prev))
endif
```

### `document.getBlockIndex(block)`

Returns the zero-based position of `block` in `document.blocks`.

- returns `-1` if the block is `null` or no longer exists in the document

This is useful when you want to remember a block object and then loop from that point onward with indexed access.

```txt
let startIndex = document.getBlockIndex(startBlock)
if startIndex < 0 then
    stop
endif

for i = startIndex to document.blockCount - 1
    let block = document.blocks[i]
    print block.text
next i
```

### `document.insertBlockAfter(block, type, text)`

- `block` — a block object (see note above). Pass `null` to insert at the end of the document.
- `type` should be `TEXT`, `SUMMARY`, `LORE`, or `OUTLINE`
- `text` is optional

Returns the newly inserted block object when successful.

Example:

```txt
let newBlock = document.insertBlockAfter(block, SUMMARY, "summary text")
```

### `document.deleteBlock(block)`

Deletes the referenced block. Accepts a block object only.

### `document.deleteBlocks(blockArray)`

Deletes all blocks in the given array of block objects.

- duplicate block references are ignored
- `null` entries are ignored
- blocks are deleted in reverse document order so the operation is safe for arrays collected during iteration

Returns the number of blocks actually deleted.

```txt
let doomed = []
for block in document
    if block.type == SUMMARY and len(trim(block.text)) == 0 then
        doomed = append(doomed, block)
    endif
next block

let n = document.deleteBlocks(doomed)
print "Deleted " + str(n) + " block(s)"
```

### `document.deleteBlocksAfter(block)`

Deletes all blocks strictly after the given block.

- pass a block object to keep that block and delete everything below it
- pass `null` to delete all blocks in the document

Returns the number of blocks actually deleted.

```txt
document.deleteBlocksAfter(null)
document.deleteBlocksAfter(draftAnchor)
```

### `document.saveIncremental(postfix?, filename?, overwrite?)`

Saves an incremental/backup copy of the document.
Note for AI agents: Most scripts do not need to create a backup copy of the document.

| Argument | Type | Default | Description |
|----------|------|---------|-------------|
| `postfix` | string | `""` → `"step"` | Suffix for auto-numbered file saved next to the document: `doc_step_1.pgr`, `doc_step_2.pgr`, … |
| `filename` | string | `""` | If non-empty, save to `Documents\Paragrapher\saves\<filename>` instead |
| `overwrite` | bool | `false` | Named save only — if `false`, skip silently when the file already exists; if `true`, overwrite |

Returns `true` on success, `false` on failure (e.g. document not yet saved to disk).

```txt
document.saveIncremental("draft")
document.saveIncremental("", "checkpoint.pgr")
document.saveIncremental("", "final.pgr", true)
```

### `document.setLive(index)`

Sets the live variation on every block to the given index.

- `0` → the latest (last) variation on each block
- `1` → variation at index 0, `2` → variation at index 1, etc. (1-based, same as agent recipe)

Each block is clamped to its own variation count, so blocks with fewer variations are not skipped.

Returns `true` if any block changed.

### `document.collapse()`

Removes all variations from every block except the live one. The live variation becomes the sole version.

Equivalent to the agent recipe `Collapse` step. Use before `splitParagraphs()` when the document has multiple variations.

Returns `true` if any block was changed.

### `document.splitParagraphs()`

Splits each text block into potentially multiple blocks using the same chunking algorithm applied on initial import.

Only text blocks with a single variation are split; blocks with multiple variations are left unchanged. Requires the document to be collapsed first.

Returns `true` if any block was split.

### `document.addSummaryPoints(prefix?)`

Inserts a summary-point block before every text block whose content starts with `prefix`. Defaults to `"Chapter"` if omitted.

Returns the number of summary-point blocks inserted.

Equivalent to the **Tools → Add Summary Points** menu command.

```txt
let n = document.addSummaryPoints("Chapter")
print "Inserted " + str(n) + " summary points"
```

## `input`

The `input` object builds a modal dialog at runtime using string ids for field lookup.

Typical flow:

1. `input.begin(title)`
2. `input.add...(...)`
3. `input.show()`
4. `input.getText(id)` / `input.getBool(id)`

Methods:

- `input.begin(title?)`
- `input.addInfo(text, uxHeight?, optional_label?)`
- `input.addText(id, label, defaultText?)`
- `input.addMultiLine(id, label, defaultText?, uxHeight?)`
- `input.addSelector(id, label, options, defaultValue?)`
- `input.addCheckbox(id, label, defaultValue?)`
- `input.show()`
- `input.getText(id)`
- `input.getBool(id)`

### `input.begin(title?)`

Clears the current form definition and sets the dialog title. If omitted, the title becomes `"Input"`.

### `input.addInfo(text, uxHeight?, optional_label?)`

Adds a read-only info area. `uxHeight` defaults to `2`. If `optional_label` is provided, it is shown above the info text.

### `input.addText(id, label, defaultText?)`

Adds a labeled single-line text field.

### `input.addMultiLine(id, label, defaultText?, uxHeight?)`

Adds a labeled multiline text field. `uxHeight` defaults to `5`.

### `input.addSelector(id, label, options, defaultValue?)`

Adds a one-line selector control with left/right buttons.

- `options` is a semicolon-separated string such as `"overwrite;new variation"`
- if `defaultValue` is omitted, the first option is selected

### `input.addCheckbox(id, label, defaultValue?)`

Adds a checkbox row.

### `input.show()`

Shows the dialog. Returns `true` when the user presses OK, `false` on Cancel.

### `input.getText(id)`

Returns the current string value for a text, multiline, or selector field.

Returns `null` if the id does not exist.

### `input.getBool(id)`

Returns the checkbox value for `id`.

## `document.blocks`

An indexable collection of block objects.

Properties:

- `document.blocks.count`
- `document.blocks.length`

Indexing:

```txt
let first = document.blocks[0]
```

## `block`

Blocks are yielded by:

- `for block in document`
- indexing into `document.blocks`

Properties:

- `block.type`
- `block.text`
- `block.label`
- `block.variationCount`
- `block.variations`
- `block.id`
- `block.liveIndex`
- `block.expanded`

Writable properties:

- `block.text`
- `block.label`
- `block.expanded`

### `block.type`

Returns one of:

- `"Text"`
- `"Summary"`
- `"Lore"`

Compare it against:

- `TEXT`
- `SUMMARY`
- `LORE`
- `OUTLINE`

### `block.text`

Gets/sets the currently selected draft text for the block.

### `block.label`

Gets/sets `breakBefore`.

### `block.expanded`

Gets/sets whether a Summary/Lore/Outline block is expanded in the UI. When `true`, the full content is visible; when `false` (default), it's compacted to a preview.

Example:

```txt
// Expand all lore blocks
for block in document
    if block.type == LORE then
        block.expanded = true
    endif
next block
```

### `block.variationCount`

Number of variations on the block.

### `block.variations`

Collection of variation objects.

### `block.id`

Internal numeric block id. Starts from 1, not guaranteed to be contiguous. Use this value when you need to have an unique number representing a block. Not the same as a positional index.

Methods:

- `block.addVariation(text)`
- `block.removeVariation(index)`
- `block.setLive(index)`
- `block.getVariation(index)`

### `block.addVariation(text)`

Adds a new variation using the given text. Important: The document will also set this variation as live.

### `block.removeVariation(index)`

Removes a variation by index.

### `block.liveIndex`

Returns the 1-based index of the currently live variation for this block (the variation returned by `block.text`).

### `block.setLive(index)`

Sets the live variation for this block only.

- `0` → the latest (last) variation on the block
- `1` → first variation, `2` → second variation, etc.

The requested variation is clamped to the block's variation count.

```txt
for block in document
    if block.variationCount > 1 then
        print block.id & " live=" & block.liveIndex
        block.setLive(1)
    endif
next block
```

### `block.getVariation(index)`

Returns a variation object using version-style indexing.

- `0` → the latest (last) variation on the block
- `1` → first variation, `2` → second variation, etc.

Returns `null` if the requested variation does not exist.

```txt
let first = block.getVariation(1)
let latest = block.getVariation(0)
if first != null then
    print first.text
endif
```

## `block.variations`

Indexable/iterable collection of variation objects.

Important:

- `block.variations[...]` uses normal array-style indexing and is `0`-based
- `block.getVariation(index)` and `block.setLive(index)` use version-style indexing and are `1`-based, with `0 = latest`

So:

- `block.variations[0]` = first variation
- `block.variations[1]` = second variation
- `block.getVariation(1)` = first variation
- `block.getVariation(2)` = second variation
- `block.getVariation(0)` = latest variation

Properties:

- `block.variations.count`
- `block.variations.length`

Indexing:

```txt
let v = block.variations[0]
```

Iteration:

```txt
for v in block.variations
    print v.text
next v
```

## `variation`

Properties:

- `variation.text`

Writable:

- `variation.text`

## Looping Examples

### Iterate all blocks

```txt
for block in document
    print block.text
next block
```

### Add label to summary blocks

```txt
for block in document
    if block.type == SUMMARY then
        block.label = "Summary"
    endif
next block
```

### Add a variation to text blocks

```txt
for block in document
    if block.type == TEXT then
        block.addVariation(block.text & " revised")
    endif
next block
```

### Insert summary before chapter blocks

```txt
for block in document
    if block.type == TEXT then
        if startsWith(block.text, "Chapter") then
            document.insertBlockAfter(block, SUMMARY, "")
        endif
    endif
next block
```

### Apply a plain instruction to every text block (simple)

`llm.applyLive` is the simplest way to run an instruction over text blocks without any prompt-flow wiring.

```txt
// Add a tightened variation to every text block
for block in document
    if block.type == TEXT then
        llm.applyLive(block, "Tighten this paragraph — cut filler words.", true)
    endif
next block
```

```txt
// Overwrite the live variation in-place
for block in document
    if block.type == TEXT then
        llm.applyLive(block, "Convert all dialogue tags to past tense.")
    endif
next block
```

### Rewrite all text blocks using the app prompt-flow settings (mirrors agent LLM loop)

```txt
for block in document
    if block.type == TEXT then
        let lore = document.getLore(block)
        let summaries = document.getSummaries(block)
        let prev = document.getPreviousBlocksText(block, 3)

        llm.reset()
        llm.systemPrompt = promptFlow.buildSystemPrompt(lore)

        if len(summaries) > 0 then
            llm.addUser(promptFlow.summariesBridgePrompt)
            llm.addAssistant(promptFlow.buildSummariesAssistantBlock(summaries))
        endif

        if len(prev) > 0 then
            llm.addUser(promptFlow.previousParagraphsBridgePrompt)
            llm.addAssistant(promptFlow.buildPreviousParagraphsAssistantBlock(prev))
        endif

        let result = llm.call(promptFlow.buildFinalUserPrompt(promptFlow.rewritePrompt, block.text))
        if len(result) > 0 then
            block.addVariation(result)
        endif
    endif
next block
```

### Summarize all summary blocks (simple)

`llm.summarize(block)` builds the full summary request using the app's prompt-flow settings, exactly as the agent runner and RightPane do, and returns the LLM result text. An optional second argument controls whether lore context is included (default `true`).

```txt
for block in document
    if block.type == SUMMARY then
        let result = llm.summarize(block)
        if len(result) > 0 then
            block.text = result
        endif
    endif
next block
```

### Summarize all summary blocks (manual alternative)

The equivalent written out step by step. Useful when you need to customise the system prompt, swap the model, or inspect intermediate values.

```txt
for block in document
    if block.type == SUMMARY then
        let lore = document.getLore(block)
        let rangeText = document.collectSummaryText(block)

        if len(rangeText) > 0 then
            llm.reset()
            llm.systemPrompt = promptFlow.buildSummarizerSystemPrompt(lore)

            let result = llm.call(promptFlow.buildSummaryFinalUserPrompt(promptFlow.summaryPrompt, "", rangeText))
            if len(result) > 0 then
                block.text = result
            endif
        endif
    endif
next block
```

## Notes and Limitations

- The language is currently case-sensitive.
- Newlines terminate statements.
- `if` requires `then`.
- `while` closes with `wend`.
- `for` closes with `next`.
- `stop` stops the script immediately without error.
- `break` exits the nearest loop.
- `continue` skips to the next iteration of the nearest loop.
- `for/in` iterates objects that support iteration.
- `for range` supports optional `step`.
- `next` may be written as bare `next` or `next variableName`.
- if a variable name is present after `next`, it must match the loop variable.
- `step 0` is an error.
- `llm.call(...)` executes real non-streaming LLM requests.

## Current Highlighting Categories

The editor currently highlights:

- keywords
- strings
- comments
- numbers
- builtins (global function names, `document`, `llm`, `promptFlow`)
  `input` is also highlighted as a built-in global
- constants (`TEXT`, `SUMMARY`, `LORE`, `OUTLINE`)
- methods (any `.name(...)` call — coral/red)

Current constants:

- `TEXT`
- `SUMMARY`
- `LORE`
- `OUTLINE`

Current built-in global highlighted names:

- `document`
- `llm`
- `promptFlow`
- `input`
