Configuration
twee-ts automatically loads twee-ts.config.json from the current working directory. Use -c <file> to specify a different path, or --no-config to skip loading entirely.
Paths in a config file (sources, exclude, output, modules, headFile, formatPaths) are relative to the folder that holds the config file, so twee-ts -c proj/twee-ts.config.json from the folder above proj builds proj/src/, not src/. Absolute paths and "-" (stdout) are kept as they are. (Before 2.0 they were relative to the working directory; see Migrating to 2.0.) Paths given on the command line stay relative to the working directory.
JSON Schema
The config file has a JSON Schema that provides autocompletion, validation, and inline documentation in editors like VS Code. Add a $schema key to enable it:
{
"$schema": "https://unpkg.com/@rohal12/twee-ts/schemas/twee-ts.config.schema.json",
"sources": ["src/"],
"output": "story.html"
}SchemaStore lists the schema too, so editors that use SchemaStore will automatically associate twee-ts.config.json files with it — no $schema key needed. SchemaStore does not keep a copy: its entry points to the schema in the npm package (https://unpkg.com/@rohal12/twee-ts/schemas/twee-ts.config.schema.json), so the schema an editor uses is always the one of the latest release. To validate against the schema of the version you have installed, use a $schema key with a version in the URL (https://unpkg.com/@rohal12/twee-ts@2/schemas/twee-ts.config.schema.json).
Minimal Config
{
"sources": ["src/"],
"output": "story.html"
}With this in place, run npx @rohal12/twee-ts with no arguments.
Complete Reference
Every key, with its default. sources has none: name the sources here or on the command line. formatId and startPassage override StoryData's format and start passage (the values shown are what a story gets when neither names one), so leave them out to let StoryData decide.
{
"sources": ["src/"],
"exclude": [],
"output": "-",
"outputMode": "html",
"formatId": "sugarcube-2",
"startPassage": "Start",
"formatPaths": [],
"formatIndices": [],
"formatUrls": [],
"useTweegoPath": true,
"modules": [],
"headFile": "head.html",
"trim": true,
"twee2Compat": false,
"testMode": false,
"noRemote": false,
"formatFetchTimeout": 30000,
"formatResolutionTimeout": 120000,
"useDefaultFormatIndices": true,
"tagAliases": {},
"sourceInfo": false,
"wordCountMethod": "tweego"
}Field Reference
Sources & Output
| Key | Type | Default | Description |
|---|---|---|---|
sources | string[] | — | Files or directories to compile. Directories are walked recursively. Required here or on the command line. |
exclude | string[] | [] | Glob patterns for source files to leave out. See Excluding files. |
output | string | "-" | Output file path; "-" for stdout. Must not be empty. |
outputMode | string | "html" | One of html, twee3, twee1, twine2-archive, twine1-archive, json. |
Source directories are walked as on the command line: a symbolic link to a directory inside one is not followed, and the output file is never read as a source. See the CLI Reference.
Excluding files
Like Tweego, twee-ts loads every file it supports from sources: Twee, CSS, JavaScript, fonts, and images, audio and video, which become base64 media passages. To keep files out, such as artwork stored next to the passages that use it but served separately, list glob patterns in exclude:
{
"sources": ["src/story"],
"exclude": ["src/story/**/*.png"]
}- Each pattern is matched against a file's path relative to the working directory, the path
--log-filesprints, with Node'spath.matchesGlob:*matches within one folder,**across folders,{png,webp}either name. A leading./is ignored.*and**don't match names that start with a dot. In a config file, a relative pattern is relative to the config file's folder; on the command line, to the working directory. - The path is also matched as the real path relative to the working directory, so a pattern works whether the working directory or the sources are reached through a symbolic link (macOS's
/varand/private/var, a linked project folder). A pattern that starts with/(or a drive letter) is matched against absolute paths. - The file extension is matched without regard to letter case, as twee-ts reads
Photo.PNGas a PNG image:**/*.pngleaves it out. On a volume that ignores letter case (macOS and Windows by default), the whole path is. - To leave out a folder, match the files in it:
src/story/art/**. **doesn't reach outside the working directory. For sources outside it, start the pattern with the same../path:../shared/**/*.png.- A file listed in
sourcesdirectly is left out too if a pattern matches it. Modules and the head file are never left out. - In watch mode and in the Vite plugin's dev server, changes to excluded files don't trigger a rebuild.
--excludeon the command line replaces the config's list.
Story Format
| Key | Type | Default | Description |
|---|---|---|---|
formatId | string | "sugarcube-2" | Story format ID. Without it, StoryData's format and format-version decide, and sugarcube-2 only when StoryData names none. Must not be empty. |
formatPaths | string[] | [] | Extra format directories, which outrank TWEEGO_PATH. |
formatIndices | string[] | [] | http:/https: URLs of SFA-compatible index.json files, consulted after formatUrls and before the Story Formats Archive. |
formatUrls | string[] | [] | http:/https: URLs of format.js files, consulted after local formats and before formatIndices. |
useTweegoPath | boolean | true | Also search the TWEEGO_PATH environment variable for formats. |
noRemote | boolean | false | Disable remote format fetching entirely. |
formatFetchTimeout | number | 30000 | Milliseconds each story format request may take before it fails with a warning and the next source is tried. 0 turns the limit off. |
formatResolutionTimeout | number | 120000 | Milliseconds the search for the story format may take in all. When it passes, the request in progress stops with a warning, and the format URLs and indices not yet asked answer from the download cache only. 0 turns the limit off. |
useDefaultFormatIndices | boolean | true | Ask the Story Formats Archive after formatIndices. false asks only the configured format URLs and indices. |
Compilation
| Key | Type | Default | Description |
|---|---|---|---|
startPassage | string | "Start" | Name of the starting passage. Without it, StoryData's start decides, and Start only when StoryData names none. Must not be empty. |
trim | boolean | true | Trim leading and trailing whitespace from passage content, in Twee and HTML sources. |
twee2Compat | boolean | false | Enable Twee2 syntax compatibility mode. |
testMode | boolean | false | Enable test/debug mode (sets the debug option in story data). |
sourceInfo | boolean | false | Embed source file/line as data- attributes on passage elements. |
wordCountMethod | string | "tweego" | Word counting: "tweego" (characters / 5 after NFKD normalization, matches Tweego) or "whitespace" (words between white space, after removing comments and markup). |
The "whitespace" method counts the words a reader sees when SugarCube 2 shows the passage: the runs of characters between white space in the text it shows. Passage markup is read as SugarCube's wikifier reads it, leftmost first:
- Macro tags, opening and closing, give no words. A tag runs from
<<and a macro name to the first>>outside a quoted string, comment or link markup, so a>or>=operator, or a>or>>in a string, doesn't end it (<<if $score >= 10>>,<<set $label to "A > B">>). Text that starts no macro tag, such as<< x >>or a<<with no>>after it, is prose. The text in every branch of<<if>>and other containers counts. - Comments (
/* */,/% %/,<!-- -->) give no words. A comment that isn't closed loses only its opener, as SugarCube drops it, and the text after it counts. - Link markup counts as the text the link shows (
[[Go home|Home]]counts asGo home). - HTML start tags that SugarCube's
htmlTagparser reads give no words, whatever their quoted attribute values hold (<span title="score > 10">), and a<<or[[inside one is part of the tag. An end tag gives none when it closes the innermost such element; inside an element, the end tag of an outer one is text, as SugarCube reads an element's content up to its own end tag. A<that starts no tag is prose (I <3 you,A < B and C > D), and so is markup SugarCube shows as text, such as a tag left open (Well done <b), an end tag nothing opened, or<!x>. A<style>element gives no words, an<svg>element and the content of<html>…</html>are each read as HTML of their own (so nothing left open inside reaches the text after them), and the content of<nowiki>…</nowiki>as text, as SugarCube's own parsers for them read them; one without its end loses only its opener. SugarCube's parsers for<styleand<svgalso take a longer tag name (<styles>,<svg-icon>), whose element is read the same way, as HTML of its own. - What is left is shown as SugarCube makes it, one element at a time, and the text a browser shows of it counts. SugarCube makes each HTML element from its own start tag, in the context of the element around it, so a table part outside a table (
<tr>,<td>) gives no element of its own and its content counts as it is, and the same tag gives another element inside atable. A line feed is a line break, which separates words, except directly inside atable,ul,ol,dlor another element SugarCube'shtmlTagparser takes no line breaks in (<ul>one\ntwo</ul>is one word; in a<li>or<td>in it, a line feed separates words again). A character reference with its;counts as the character it stands for (one twois two words), one without it is text (one   twois three), a<br>separates words, and the text ofscript,style,title,noscriptandtemplateelements doesn't count; that of atextareadoes. In SVG only the text oftextelements (and thetspan,textPathandaelements in them) and offoreignObjectcounts, nothing insidedefsand the like, or in an element SVG does not define. In passage markup no text inside<math>counts, and nor does any HTML or SVG element in it, since SugarCube makes the elements in it HTML elements, which MathML doesn't render. The content of<html>…</html>and<svg>elements is read as jQuery appends it (a first tag that is a table part is read inside a table, so text before a<tr>is lost), and shown as a browser shows it: the text of MathML token elements (mi,mn,mo,ms,mtext) in it counts. White space between elements always separates the words around it, also where a browser shows no text.
Words on either side of any other tag or a macro tag with no white space between them count as one (a<b>b</b> is one word). Styles, hidden attributes and what macros print are not taken into account. SugarCube's other markup counts as written: the * and # of list items (line markup, not the <ul> element), a > that starts a block quote, a ---- rule, the | of table rows, a \ that continues a line, and the """ around verbatim text. A passage whose HTML nests elements more than 512 deep (Chromium's limit, counting the page's html and body elements and the element the passage is shown in), which a passage can do by opening an element in each branch of an <<if>>, is counted as its text without tags, with a warning naming the passage; the build goes on.
Head Injection
| Key | Type | Default | Description |
|---|---|---|---|
modules | string[] | [] | JS or CSS files to inject into the HTML <head>. |
headFile | string | — | Path to a raw HTML file whose contents are appended to <head>. Must not be empty. Keep it out of the source folders, where an .html file is read as a Twine story. |
See Head Injection for where they go in a template without a closing head tag.
Tag Aliases
| Key | Type | Default | Description |
|---|---|---|---|
tagAliases | Record<string, string> | {} | Map custom tag names to canonical special tags. Both are non-empty and hold no whitespace. |
See the dedicated Tag Aliases page for full details and examples.
CLI Override
CLI flags take precedence over config file values. For example:
{
"sources": ["src/"],
"formatId": "sugarcube-2",
"startPassage": "Begin"
}# Overrides formatId to harlowe-3, keeps startPassage as Begin
twee-ts -f harlowe-3For tagAliases, CLI --tag-alias flags are merged on top of config values, so you can set defaults in the config and add or override aliases per invocation. Every other list on the command line (--exclude, -m, --format-url, --format-index) replaces the config's. See Precedence.
Scaffolding
Run npx @rohal12/twee-ts --init to generate a starter config and source files:
npx @rohal12/twee-ts --initThis creates:
twee-ts.config.jsonwith$schema,sources(["src/"]) andoutput("story.html")src/StoryData.twwith a generated IFIDsrc/StoryTitle.twwith the titleMy Story(SugarCube refuses to start a story without one)src/Start.twwith a starter passage
and reports:
Initializing new twee-ts project...
Created:
twee-ts.config.json
src/StoryData.tw
src/StoryTitle.tw
src/Start.tw
Run: npx @rohal12/twee-tsFiles that already exist are kept as they are and reported as skipped (Skipped (already exists): src/Start.tw), so running --init in an existing project never replaces your story or its IFID.
Validation
twee-ts validates the config file on load. A value of the wrong type stops the build (exit status 1) with an error that lists every problem:
error: Invalid config in /path/to/twee-ts.config.json:
"sources" must be an array.
"trim" must be a boolean.The config file is read as strict JSON; a file that is not is an error giving the line and column. A key given twice is a warning ($.sources is given more than once; the last one is used.), and the last one is used. The checks are those of the JSON Schema, which is generated from the same table twee-ts checks with, so the two agree.
A key the config does not define is ignored, so its option keeps the default. twee-ts warns about each one and the build goes on. When the key differs from a real one only in letter case, - or _, the warning names the real key:
warning: /path/to/twee-ts.config.json: Unknown config key "formatID" (did you mean "formatId"?); it is ignored.$schema is always allowed.
The config file is read like any other text file: it may start with a byte order mark, be UTF-16 (as Windows PowerShell 5's > writes it) and use CRLF line endings. See Text encoding.