diff --git a/.gitignore b/.gitignore index 49f3169..ef7fce9 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ spec/* !spec/.gitkeep !spec/README.md !spec/get_specs.sh +!spec/token_system.md # Core dumps core diff --git a/CLAUDE.md b/CLAUDE.md index 257fffc..944a6d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,7 +59,7 @@ Commands are emitted via `emit_task()` in `src/commands.c`, which prints shell s **Fuzzy Matching** (`src/fuzzy.c`, `src/fuzzy.h`): - `fuzzy_match()`: Updates TryEntry score and rendered output in-place - `calculate_score()`: Combines fuzzy match score with recency (mtime) -- `highlight_matches()`: Inserts `{b}` tokens around matched characters +- `highlight_matches()`: Inserts `{highlight}` tokens around matched characters - Algorithm favors consecutive character matches and recent access times - **Documentation**: See `spec/fuzzy_matching.md` for complete algorithm specification @@ -69,8 +69,13 @@ Commands are emitted via `emit_task()` in `src/commands.c`, which prints shell s - Window size detection - Cursor visibility control +**Token System** (`src/tokens.rl`, `src/tokens.c`, `src/tokens.h`): +- Ragel-based state machine for token expansion +- Stack-based style nesting with `{/}` pop operation +- Full color palette support (standard, bright, 256-color) +- Control sequences for cursor and screen management + **Utilities** (`src/utils.c`, `src/utils.h`): -- Token expansion: Replaces `{b}`, `{reset}`, etc. with ANSI codes via `zstr_expand_tokens()` - Path utilities: `join_path()`, `mkdir_p()`, directory existence checks - Time formatting: `format_relative_time()` for human-readable timestamps - `AUTO_FREE` macro: Cleanup helper for raw pointers using `Z_CLEANUP()` @@ -108,47 +113,59 @@ Manual cleanup still required in some cases: ### Token System -The UI uses a token-based formatting system for dynamic styling. Tokens are placeholder strings embedded in text that get expanded to ANSI escape codes via `zstr_expand_tokens()` in `src/utils.c`. This allows formatting to be defined declaratively without hardcoding ANSI sequences throughout the codebase. +The UI uses a stack-based token formatting system implemented with [Ragel](https://www.colm.net/open-source/ragel/). Tokens are placeholder strings embedded in text that get expanded to ANSI escape codes via `zstr_expand_tokens()`. This allows formatting to be defined declaratively without hardcoding ANSI sequences throughout the codebase. + +**Implementation**: `src/tokens.rl` (Ragel source) generates `src/tokens.c` **Documentation**: See `spec/token_system.md` for complete token specifications and usage patterns. +**Stack Semantics**: Each style token pushes its previous state onto a stack. `{/}` pops one level, restoring the previous state. This enables proper nesting of styles. + +**Key Features:** +- **Deferred Emission**: ANSI codes only emit when an actual character is printed +- **Redundancy Avoidance**: Repeated identical styles don't emit duplicate codes +- **Auto-Reset at Newlines**: All styles automatically reset before each newline + **Available Tokens:** -| Token | ANSI Code | Description | -|-------|-----------|-------------| -| `{h1}` | Bold + Orange (38;5;214m) | Primary headings | -| `{h2}` | Bold + Blue | Secondary headings | -| `{b}` | Bold + Yellow | Bold/highlighted text, fuzzy matches | -| `{/b}` | Reset bold and foreground | End bold formatting and reset color | -| `{dim}` | Bright black (gray, 90m) | Dimmed/secondary text | -| `{text}` | Reset | Normal text (full reset) | -| `{reset}` | Reset | Full reset of all formatting | -| `{/fg}` | Reset foreground | Reset foreground color only | -| `{section}` | Bold | Start of selected section | -| `{/section}` | Reset | End of selected section | - -**ANSI Constants** (defined in `src/utils.h`): -- `ANSI_RESET` - "\033[0m" -- `ANSI_BOLD` - "\033[1m" -- `ANSI_DIM` - "\033[2m" -- `ANSI_RED`, `ANSI_GREEN`, `ANSI_YELLOW`, `ANSI_BLUE`, `ANSI_MAGENTA`, `ANSI_CYAN`, `ANSI_WHITE` +| Category | Tokens | +|----------|--------| +| **Semantic** | `{b}` (bold), `{highlight}` (bold+yellow), `{h1}` (bold+orange), `{h2}` (bold+blue), `{dim}` (gray), `{section}` (bold), `{danger}` (red bg) | +| **Attributes** | `{bold}` `{B}`, `{italic}` `{I}`, `{underline}` `{U}`, `{reverse}`, `{strikethrough}`, `{strike}` | +| **Colors** | `{red}`, `{green}`, `{blue}`, `{yellow}`, `{cyan}`, `{magenta}`, `{white}`, `{black}`, `{gray}`/`{grey}` | +| **Bright Colors** | `{bright:red}`, `{bright:green}`, etc. | +| **256-Color** | `{fg:N}`, `{bg:N}` where N is 0-255 | +| **Background** | `{bg:red}`, `{bg:green}`, etc. | +| **Reset/Pop** | `{/}` (pop), `{/name}` (e.g., `{/highlight}`), `{reset}` (full reset), `{/fg}`, `{/bg}` | +| **Control** | `{clr}` (clear line), `{cls}` (clear screen), `{home}`, `{hide}`, `{show}` | +| **Special** | `{cursor}` (cursor position tracking) | **Usage Pattern:** ```c -Z_CLEANUP(zstr_free) zstr message = zstr_from("Status: {b}OK{/b}"); -Z_CLEANUP(zstr_free) zstr expanded = zstr_expand_tokens(zstr_cstr(&message)); -printf("%s\n", zstr_cstr(&expanded)); +// Simple formatting +Z_CLEANUP(zstr_free) zstr result = zstr_expand_tokens("Status: {b}OK{/}"); + +// Nested styles (stack-based) +Z_CLEANUP(zstr_free) zstr result = zstr_expand_tokens("{bold}Bold {red}and red{/} just bold{/} normal"); + +// 256-color support +Z_CLEANUP(zstr_free) zstr result = zstr_expand_tokens("{fg:214}Orange text{/}"); ``` **In Fuzzy Matching:** -The fuzzy matching system in `src/fuzzy.c` inserts `{b}` tokens around matched characters. These tokens are preserved through the rendering pipeline and expanded to ANSI codes when displayed: +The fuzzy matching system in `src/fuzzy.c` inserts `{highlight}` tokens around matched characters. These tokens are preserved through the rendering pipeline and expanded to ANSI codes when displayed: ```c // Input: "2025-11-29-test", query: "te" -// Output: "2025-11-29-{b}te{/b}st" +// Output: "2025-11-29-{highlight}te{/}st" // Displayed: "2025-11-29-[bold yellow]te[reset]st" ``` +**Regenerating Token Parser:** +```bash +ragel -C -G2 src/tokens.rl -o src/tokens.c +``` + ## Configuration Constants defined in `src/config.h`: @@ -218,10 +235,12 @@ Z_CLEANUP(zstr_free) zstr expanded = zstr_expand_tokens(zstr_cstr(&formatted)); External dependencies are minimal: - Standard C library (POSIX) - Math library (`-lm` for fuzzy scoring) -- No external libraries beyond libc +- Ragel (`ragel`) - only needed if modifying `src/tokens.rl` The `src/libs/` directory contains bundled single-header libraries (zstr, zvec, zlist) that are self-contained. +Note: The generated `src/tokens.c` is checked into the repository, so Ragel is only required when modifying the token system. + ## Directory Structure - `src/` - C source and header files @@ -239,7 +258,9 @@ The `src/libs/` directory contains bundled single-header libraries (zstr, zvec, - `src/main.c` - Entry point, argument parsing - `src/tui.c` - Interactive selector implementation - `src/fuzzy.c` - Scoring and highlighting logic -- `src/utils.c` - Shared utilities, token expansion +- `src/tokens.rl` - Token system (Ragel source) +- `src/tokens.c` - Token system (generated, do not edit) +- `src/utils.c` - Shared utilities - `src/commands.c` - Command implementations, shell emission - `Makefile` - Build configuration - `docs/try.reference.rb` - Ruby reference implementation (source of truth for features) @@ -248,9 +269,11 @@ The `src/libs/` directory contains bundled single-header libraries (zstr, zvec, **IMPORTANT**: When making changes to certain subsystems, their corresponding documentation files must be updated: -- **Token expansion** (`src/utils.c`, `zstr_expand_tokens()`): +- **Token system** (`src/tokens.rl`): - Update `spec/token_system.md` with any new tokens or changed ANSI codes - Update the token table in this file (CLAUDE.md) + - Update `src/tokens.h` header documentation + - Regenerate C code: `ragel -C -G2 src/tokens.rl -o src/tokens.c` - **Fuzzy matching algorithm** (`src/fuzzy.c`, `fuzzy_match()`): - Update `spec/fuzzy_matching.md` with algorithm changes diff --git a/Makefile b/Makefile index e0c94ca..415f035 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,27 @@ DIST_DIR = dist BIN = $(DIST_DIR)/try SRCS = $(wildcard $(SRC_DIR)/*.c) -OBJS = obj/commands.o obj/main.o obj/terminal.o obj/tui.o obj/utils.o obj/fuzzy.o +OBJS = obj/commands.o obj/main.o obj/terminal.o obj/tui.o obj/utils.o obj/fuzzy.o obj/tokens.o + +# Ragel-generated sources (tokens.c is checked in, ragel only needed for modifications) +RAGEL_SRCS = $(SRC_DIR)/tokens.c all: $(BIN) +# Generate C from Ragel (only if .rl is newer than .c AND ragel is available) +# tokens.c is read-only to prevent accidental edits - modify tokens.rl instead +$(SRC_DIR)/tokens.c: $(SRC_DIR)/tokens.rl + @if command -v ragel >/dev/null 2>&1; then \ + chmod +w $@ 2>/dev/null || true; \ + ragel -C -G2 $< -o $@; \ + chmod -w $@; \ + echo "Regenerated tokens.c from tokens.rl"; \ + else \ + echo "WARNING: ragel not installed. Using existing tokens.c"; \ + echo " Install ragel if you need to modify tokens.rl"; \ + touch $@; \ + fi + $(BIN): $(OBJS) | $(DIST_DIR) $(CC) $(LDFLAGS) -o $@ $^ -lm @@ -30,6 +47,11 @@ $(DIST_DIR): clean: rm -rf $(OBJ_DIR) $(DIST_DIR) +# tokens.o depends on the generated tokens.c +# Ragel-generated code has intentional switch fallthrough and unused variables +$(OBJ_DIR)/tokens.o: $(SRC_DIR)/tokens.c | $(OBJ_DIR) + $(CC) $(CFLAGS) -Wno-unused-const-variable -Wno-implicit-fallthrough -c -o $@ $< + install: $(BIN) install -m 755 $(BIN) /usr/local/bin/try @@ -45,7 +67,12 @@ test-valgrind: $(BIN) spec-update @echo "Running spec tests under valgrind..." spec/upstream/tests/runner.sh "valgrind -q --leak-check=full ./dist/try" -test: test-fast +test-unit: $(OBJ_DIR)/tokens.o | $(DIST_DIR) + @echo "Building and running unit tests..." + $(CC) $(CFLAGS) -o $(DIST_DIR)/tokens_test src/tokens_test.c $(OBJ_DIR)/tokens.o + $(DIST_DIR)/tokens_test + +test: test-unit test-fast @command -v valgrind >/dev/null 2>&1 && $(MAKE) test-valgrind || echo "Skipping valgrind tests (valgrind not installed)" # Update PKGBUILD and .SRCINFO with current VERSION @@ -54,4 +81,4 @@ update-pkg: @makepkg --printsrcinfo > .SRCINFO @echo "Updated PKGBUILD and .SRCINFO to version $(VERSION)" -.PHONY: all clean install test test-fast test-valgrind spec-update update-pkg +.PHONY: all clean install test test-fast test-valgrind test-unit spec-update update-pkg diff --git a/flake.nix b/flake.nix index 98076f7..1926aa3 100644 --- a/flake.nix +++ b/flake.nix @@ -96,6 +96,7 @@ gnumake valgrind gdb + ragel # For modifying src/tokens.rl ]; shellHook = '' diff --git a/spec/token_system.md b/spec/token_system.md new file mode 100644 index 0000000..b04a42c --- /dev/null +++ b/spec/token_system.md @@ -0,0 +1,245 @@ +# Token System Specification + +The token system provides a declarative way to apply ANSI formatting in terminal output. It uses a stack-based approach that allows proper nesting and clean restoration of previous styles. + +> **Note**: It is totally OK to only implement a subset of this system to make try work. This spec is extensive because it's being used outside of just try. + +## Implementation + +The token parser is implemented using [Ragel](https://www.colm.net/open-source/ragel/) to generate a fast state machine. The source is in `src/tokens.rl` and generates `src/tokens.c`. + +To regenerate after modifying `tokens.rl`: +```bash +ragel -C -G2 src/tokens.rl -o src/tokens.c +``` + +## Stack Semantics + +Each style-setting token pushes its previous state onto a stack. The `{/}` token pops one level, restoring the previous state. This enables proper nesting: + +```c +// Input: "Normal {highlight}bold yellow {red}red{/} yellow{/} normal" +// Output: "Normal [bold+yellow]bold yellow [red]red[yellow] yellow[reset] normal" +``` + +Composite tokens (like `{highlight}` which sets both bold and yellow) push multiple attributes but are grouped so a single `{/}` restores all of them. + +## Deferred Emission + +ANSI codes are only emitted when an actual character is about to be printed. This provides several benefits: + +1. **Redundancy avoidance**: `{dim}{dim}{dim}x` outputs only one `\033[90m` code +2. **No unused codes**: `{b}{/}x` outputs just `x` with no ANSI codes +3. **Efficient output**: Codes are batched when possible (e.g., `\033[1;33m` instead of `\033[1m\033[33m`) + +## Auto-Reset at Newlines + +All active styles are automatically reset before each newline character. This ensures: +- Clean line boundaries without trailing ANSI codes +- Proper terminal behavior when lines are scrolled or redrawn +- No style bleeding across lines + +## Token Reference + +### Semantic Tokens (Composite Styles) + +| Token | Effect | ANSI Codes | +|-------|--------|------------| +| `{b}` | Bold only (same as `{strong}`) | `\033[1m` | +| `{highlight}` | Bold + yellow foreground (for highlighting matches) | `\033[1;33m` | +| `{h1}` | Bold + orange (256-color 214) | `\033[1;38;5;214m` | +| `{h2}` | Bold + blue | `\033[1;34m` | +| `{h3}` - `{h6}` | Bold + white | `\033[1;37m` | +| `{strong}` | Bold | `\033[1m` | +| `{dim}` | Gray foreground (bright black) | `\033[90m` | +| `{section}` | Bold + dark gray background (for selection highlight) | `\033[1;48;5;237m` | +| `{danger}` | Dark red background (for warnings/deletions) | `\033[48;5;52m` | +| `{strike}` | Legacy alias for `{danger}` | `\033[48;5;52m` | +| `{text}` | Full reset | `\033[0m` | + +### Attribute Tokens + +| Token | Effect | ANSI Code | +|-------|--------|-----------| +| `{bold}`, `{B}` | Bold text | `\033[1m` | +| `{italic}`, `{I}`, `{i}` | Italic text | `\033[3m` | +| `{underline}`, `{U}`, `{u}` | Underlined text | `\033[4m` | +| `{reverse}` | Reverse/inverse video | `\033[7m` | +| `{strikethrough}` | Strikethrough text | `\033[9m` | +| `{bright}` | Brighten current foreground color | Converts 30-37 to 90-97 | + +### Foreground Colors + +Standard colors: +| Token | ANSI Code | +|-------|-----------| +| `{black}` | `\033[30m` | +| `{red}` | `\033[31m` | +| `{green}` | `\033[32m` | +| `{yellow}` | `\033[33m` | +| `{blue}` | `\033[34m` | +| `{magenta}` | `\033[35m` | +| `{cyan}` | `\033[36m` | +| `{white}` | `\033[37m` | +| `{gray}`, `{grey}` | `\033[90m` | + +Bright colors: +| Token | ANSI Code | +|-------|-----------| +| `{bright:black}` | `\033[90m` | +| `{bright:red}` | `\033[91m` | +| `{bright:green}` | `\033[92m` | +| `{bright:yellow}` | `\033[93m` | +| `{bright:blue}` | `\033[94m` | +| `{bright:magenta}` | `\033[95m` | +| `{bright:cyan}` | `\033[96m` | +| `{bright:white}` | `\033[97m` | + +256-color palette: +| Token | ANSI Code | +|-------|-----------| +| `{fg:N}` | `\033[38;5;Nm` where N is 0-255 | + +### Background Colors + +Standard colors: +| Token | ANSI Code | +|-------|-----------| +| `{bg:black}` | `\033[40m` | +| `{bg:red}` | `\033[41m` | +| `{bg:green}` | `\033[42m` | +| `{bg:yellow}` | `\033[43m` | +| `{bg:blue}` | `\033[44m` | +| `{bg:magenta}` | `\033[45m` | +| `{bg:cyan}` | `\033[46m` | +| `{bg:white}` | `\033[47m` | +| `{bg:gray}`, `{bg:grey}` | `\033[100m` | + +256-color palette: +| Token | ANSI Code | +|-------|-----------| +| `{bg:N}` | `\033[48;5;Nm` where N is 0-255 | + +### Reset/Pop Tokens + +| Token | Effect | +|-------|--------| +| `{/}` | Pop one level, restore previous style | +| `{reset}` | Full reset, clear entire stack | +| `{/fg}` | Reset foreground color to default | +| `{/bg}` | Reset background color to default | +| `{/name}` | Same as `{/}` - name is ignored (e.g., `{/highlight}`, `{/b}`) | + +### Control Tokens + +| Token | Effect | ANSI Code | +|-------|--------|-----------| +| `{clr}` | Clear to end of line | `\033[K` | +| `{cls}` | Clear to end of screen | `\033[J` | +| `{home}` | Move cursor to home | `\033[H` | +| `{hide}` | Hide cursor | `\033[?25l` | +| `{show}` | Show cursor | `\033[?25h` | +| `{goto:row,col}` | Move cursor to position | `\033[row;colH` | +| `{goto_cursor}` | Move cursor to `{cursor}` marker position | `\033[row;colH` | + +### Special Tokens + +| Token | Effect | +|-------|--------| +| `{cursor}` | Mark cursor position for cursor tracking | + +## API + +### `zstr_expand_tokens(const char *text)` + +Expands tokens in the input text, returning a zstr with ANSI codes. + +```c +Z_CLEANUP(zstr_free) zstr result = zstr_expand_tokens("{b}Hello{/} World"); +printf("%s\n", zstr_cstr(&result)); +// Output: [bold yellow]Hello[reset] World +``` + +### `zstr_expand_tokens_with_cursor(const char *text)` + +Same as above, but also tracks the `{cursor}` marker position. + +```c +TokenExpansion result = zstr_expand_tokens_with_cursor("Search: {cursor}"); +// result.expanded contains the expanded text +// result.cursor_pos contains the visual column (1-indexed) of {cursor} +zstr_free(&result.expanded); +``` + +### Global Flags + +| Flag | Effect | +|------|--------| +| `zstr_disable_token_expansion` | If true, tokens pass through unchanged | +| `zstr_no_colors` | If true, tokens expand to empty strings (no ANSI codes) | + +## Usage Examples + +### Basic Formatting + +```c +// Simple bold highlight +zstr s = zstr_expand_tokens("Status: {b}OK{/}"); + +// Nested styles +zstr s = zstr_expand_tokens("{bold}Bold {italic}and italic{/} just bold{/} normal"); + +// Colors +zstr s = zstr_expand_tokens("{red}Error:{/} Something went wrong"); +``` + +### 256-Color Support + +```c +// Orange text (color 214) +zstr s = zstr_expand_tokens("{fg:214}Orange text{/}"); + +// Custom background +zstr s = zstr_expand_tokens("{bg:52}Dark red background{/}"); +``` + +### TUI Building + +```c +// Clear line and write header +zstr line = zstr_expand_tokens("{h1}Title{/}{clr}\n"); + +// Status with cursor tracking +TokenExpansion te = zstr_expand_tokens_with_cursor("Input: {cursor}{clr}"); +// Use te.cursor_pos to position the terminal cursor +``` + +## Design Notes + +### Why Ragel? + +Ragel generates highly optimized state machines that are: +- Fast: No backtracking, O(n) parsing +- Compact: Efficient goto-based code generation with `-G2` +- Correct: Formal grammar prevents parsing bugs + +### Stack Implementation + +The stack stores restore actions, not full state. When a token like `{bold}` is applied: +1. The previous bold state (on/off) is pushed onto the stack +2. Bold is enabled +3. When `{/}` is called, the stack is popped and the previous state is restored + +This approach minimizes memory usage and allows efficient restoration. + +### Composite Token Handling + +Tokens like `{b}` that set multiple attributes use a "composite marker" on the stack. When pushed: +1. Individual attribute changes are pushed (bold state, color state) +2. A composite marker is pushed with the count of changes + +When popped: +1. The composite marker is popped +2. All individual changes are popped and restored + +This ensures a single `{/}` can undo a composite style change. diff --git a/src/fuzzy.c b/src/fuzzy.c index f847657..4f4a17a 100644 --- a/src/fuzzy.c +++ b/src/fuzzy.c @@ -95,10 +95,10 @@ void fuzzy_match(TryEntry *entry, const char *query) { last_pos = current_pos; query_idx++; - // Append highlighted char - zstr_cat(&entry->rendered, "{b}"); + // Append highlighted char (bold+yellow) + zstr_cat(&entry->rendered, "{highlight}"); zstr_push(&entry->rendered, *orig_ptr); - zstr_cat(&entry->rendered, "{/b}"); + zstr_cat(&entry->rendered, "{/}"); } else { // No match, append regular char zstr_push(&entry->rendered, *orig_ptr); diff --git a/src/fuzzy.h b/src/fuzzy.h index 430377e..b6556d3 100644 --- a/src/fuzzy.h +++ b/src/fuzzy.h @@ -10,7 +10,7 @@ void fuzzy_match(TryEntry *entry, const char *query); // Legacy/Convenience: just calculate score (read-only) float calculate_score(const char *text, const char *query, time_t mtime); -// Highlight matching characters in text with {b} tokens +// Highlight matching characters in text with {highlight} tokens // Caller must free the returned string char *highlight_matches(const char *text, const char *query); diff --git a/src/libs/acutest.h b/src/libs/acutest.h new file mode 100644 index 0000000..5f9cb19 --- /dev/null +++ b/src/libs/acutest.h @@ -0,0 +1,1994 @@ +/* + * Acutest -- Another C/C++ Unit Test facility + * + * + * Copyright 2013-2023 Martin MitΓ‘Ε‘ + * Copyright 2019 Garrett D'Amore + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef ACUTEST_H +#define ACUTEST_H + + +/* Try to auto-detect whether we need to disable C++ exception handling. + * If the detection fails, you may always define TEST_NO_EXCEPTIONS before + * including "acutest.h" manually. */ +#ifdef __cplusplus + #if (__cplusplus >= 199711L && !defined __cpp_exceptions) || \ + ((defined(__GNUC__) || defined(__clang__)) && !defined __EXCEPTIONS) + #ifndef TEST_NO_EXCEPTIONS + #define TEST_NO_EXCEPTIONS + #endif + #endif +#endif + + +/************************ + *** Public interface *** + ************************/ + +/* By default, "acutest.h" provides the main program entry point (function + * main()). However, if the test suite is composed of multiple source files + * which include "acutest.h", then this causes a problem of multiple main() + * definitions. To avoid this problem, #define macro TEST_NO_MAIN in all + * compilation units but one. + */ + +/* Macro to specify list of unit tests in the suite. + * The unit test implementation MUST provide list of unit tests it implements + * with this macro: + * + * TEST_LIST = { + * { "test1_name", test1_func_ptr }, + * { "test2_name", test2_func_ptr }, + * ... + * { NULL, NULL } // zeroed record marking the end of the list + * }; + * + * The list specifies names of each test (must be unique) and pointer to + * a function implementing it. The function does not take any arguments + * and has no return values, i.e. every test function has to be compatible + * with this prototype: + * + * void test_func(void); + * + * Note the list has to be ended with a zeroed record. + */ +#define TEST_LIST const struct acutest_test_ acutest_list_[] + + +/* Macros for testing whether an unit test succeeds or fails. These macros + * can be used arbitrarily in functions implementing the unit tests. + * + * If any condition fails throughout execution of a test, the test fails. + * + * TEST_CHECK takes only one argument (the condition), TEST_CHECK_ allows + * also to specify an error message to print out if the condition fails. + * (It expects printf-like format string and its parameters). The macros + * return non-zero (condition passes) or 0 (condition fails). + * + * That can be useful when more conditions should be checked only if some + * preceding condition passes, as illustrated in this code snippet: + * + * SomeStruct* ptr = allocate_some_struct(); + * if(TEST_CHECK(ptr != NULL)) { + * TEST_CHECK(ptr->member1 < 100); + * TEST_CHECK(ptr->member2 > 200); + * } + */ +#define TEST_CHECK_(cond,...) \ + acutest_check_(!!(cond), __FILE__, __LINE__, __VA_ARGS__) +#define TEST_CHECK(cond) \ + acutest_check_(!!(cond), __FILE__, __LINE__, "%s", #cond) + + +/* These macros are the same as TEST_CHECK_ and TEST_CHECK except that if the + * condition fails, the currently executed unit test is immediately aborted. + * + * That is done either by calling abort() if the unit test is executed as a + * child process; or via longjmp() if the unit test is executed within the + * main Acutest process. + * + * As a side effect of such abortion, your unit tests may cause memory leaks, + * unflushed file descriptors, and other phenomena caused by the abortion. + * + * Therefore you should not use these as a general replacement for TEST_CHECK. + * Use it with some caution, especially if your test causes some other side + * effects to the outside world (e.g. communicating with some server, inserting + * into a database etc.). + */ +#define TEST_ASSERT_(cond,...) \ + do { \ + if(!acutest_check_(!!(cond), __FILE__, __LINE__, __VA_ARGS__)) \ + acutest_abort_(); \ + } while(0) +#define TEST_ASSERT(cond) \ + do { \ + if(!acutest_check_(!!(cond), __FILE__, __LINE__, "%s", #cond)) \ + acutest_abort_(); \ + } while(0) + + +#ifdef __cplusplus +#ifndef TEST_NO_EXCEPTIONS +/* Macros to verify that the code (the 1st argument) throws exception of given + * type (the 2nd argument). (Note these macros are only available in C++.) + * + * TEST_EXCEPTION_ is like TEST_EXCEPTION but accepts custom printf-like + * message. + * + * For example: + * + * TEST_EXCEPTION(function_that_throw(), ExpectedExceptionType); + * + * If the function_that_throw() throws ExpectedExceptionType, the check passes. + * If the function throws anything incompatible with ExpectedExceptionType + * (or if it does not thrown an exception at all), the check fails. + */ +#define TEST_EXCEPTION(code, exctype) \ + do { \ + bool exc_ok_ = false; \ + const char *msg_ = NULL; \ + try { \ + code; \ + msg_ = "No exception thrown."; \ + } catch(exctype const&) { \ + exc_ok_= true; \ + } catch(...) { \ + msg_ = "Unexpected exception thrown."; \ + } \ + acutest_check_(exc_ok_, __FILE__, __LINE__, #code " throws " #exctype);\ + if(msg_ != NULL) \ + acutest_message_("%s", msg_); \ + } while(0) +#define TEST_EXCEPTION_(code, exctype, ...) \ + do { \ + bool exc_ok_ = false; \ + const char *msg_ = NULL; \ + try { \ + code; \ + msg_ = "No exception thrown."; \ + } catch(exctype const&) { \ + exc_ok_= true; \ + } catch(...) { \ + msg_ = "Unexpected exception thrown."; \ + } \ + acutest_check_(exc_ok_, __FILE__, __LINE__, __VA_ARGS__); \ + if(msg_ != NULL) \ + acutest_message_("%s", msg_); \ + } while(0) +#endif /* #ifndef TEST_NO_EXCEPTIONS */ +#endif /* #ifdef __cplusplus */ + + +/* Sometimes it is useful to split execution of more complex unit tests to some + * smaller parts and associate those parts with some names. + * + * This is especially handy if the given unit test is implemented as a loop + * over some vector of multiple testing inputs. Using these macros allow to use + * sort of subtitle for each iteration of the loop (e.g. outputting the input + * itself or a name associated to it), so that if any TEST_CHECK condition + * fails in the loop, it can be easily seen which iteration triggers the + * failure, without the need to manually output the iteration-specific data in + * every single TEST_CHECK inside the loop body. + * + * TEST_CASE allows to specify only single string as the name of the case, + * TEST_CASE_ provides all the power of printf-like string formatting. + * + * Note that the test cases cannot be nested. Starting a new test case ends + * implicitly the previous one. To end the test case explicitly (e.g. to end + * the last test case after exiting the loop), you may use TEST_CASE(NULL). + */ +#define TEST_CASE_(...) acutest_case_(__VA_ARGS__) +#define TEST_CASE(name) acutest_case_("%s", name) + + +/* Maximal output per TEST_CASE call. Longer messages are cut. + * You may define another limit prior including "acutest.h" + */ +#ifndef TEST_CASE_MAXSIZE + #define TEST_CASE_MAXSIZE 64 +#endif + + +/* printf-like macro for outputting an extra information about a failure. + * + * Intended use is to output some computed output versus the expected value, + * e.g. like this: + * + * if(!TEST_CHECK(produced == expected)) { + * TEST_MSG("Expected: %d", expected); + * TEST_MSG("Produced: %d", produced); + * } + * + * Note the message is only written down if the most recent use of any checking + * macro (like e.g. TEST_CHECK or TEST_EXCEPTION) in the current test failed. + * This means the above is equivalent to just this: + * + * TEST_CHECK(produced == expected); + * TEST_MSG("Expected: %d", expected); + * TEST_MSG("Produced: %d", produced); + * + * The macro can deal with multi-line output fairly well. It also automatically + * adds a final new-line if there is none present. + */ +#define TEST_MSG(...) acutest_message_(__VA_ARGS__) + + +/* Maximal output per TEST_MSG call. Longer messages are cut. + * You may define another limit prior including "acutest.h" + */ +#ifndef TEST_MSG_MAXSIZE + #define TEST_MSG_MAXSIZE 1024 +#endif + + +/* Macro for dumping a block of memory. + * + * Its intended use is very similar to what TEST_MSG is for, but instead of + * generating any printf-like message, this is for dumping raw block of a + * memory in a hexadecimal form: + * + * TEST_CHECK(size_produced == size_expected && + * memcmp(addr_produced, addr_expected, size_produced) == 0); + * TEST_DUMP("Expected:", addr_expected, size_expected); + * TEST_DUMP("Produced:", addr_produced, size_produced); + */ +#define TEST_DUMP(title, addr, size) acutest_dump_(title, addr, size) + +/* Maximal output per TEST_DUMP call (in bytes to dump). Longer blocks are cut. + * You may define another limit prior including "acutest.h" + */ +#ifndef TEST_DUMP_MAXSIZE + #define TEST_DUMP_MAXSIZE 1024 +#endif + + +/* Macros for marking the test as SKIPPED. + * Note it can only be used at the beginning of a test, before any other + * checking. + * + * Once used, the best practice is to return from the test routine as soon + * as possible. + */ +#define TEST_SKIP(...) acutest_skip_(__FILE__, __LINE__, __VA_ARGS__) + + +/* Common test initialisation/clean-up + * + * In some test suites, it may be needed to perform some sort of the same + * initialization and/or clean-up in all the tests. + * + * Such test suites may use macros TEST_INIT and/or TEST_FINI prior including + * this header. The expansion of the macro is then used as a body of helper + * function called just before executing every single (TEST_INIT) or just after + * it ends (TEST_FINI). + * + * Examples of various ways how to use the macro TEST_INIT: + * + * #define TEST_INIT my_init_func(); + * #define TEST_INIT my_init_func() // Works even without the semicolon + * #define TEST_INIT setlocale(LC_ALL, NULL); + * #define TEST_INIT { setlocale(LC_ALL, NULL); my_init_func(); } + * + * TEST_FINI is to be used in the same way. + */ + + +/********************** + *** Implementation *** + **********************/ + +/* The unit test files should not rely on anything below. */ + +#include + +/* Enable the use of the non-standard keyword __attribute__ to silence warnings under some compilers */ +#if defined(__GNUC__) || defined(__clang__) + #define ACUTEST_ATTRIBUTE_(attr) __attribute__((attr)) +#else + #define ACUTEST_ATTRIBUTE_(attr) +#endif + +#ifdef __cplusplus + extern "C" { +#endif + +enum acutest_state_ { + ACUTEST_STATE_INITIAL = -4, + ACUTEST_STATE_SELECTED = -3, + ACUTEST_STATE_NEEDTORUN = -2, + + /* By the end all tests should be in one of the following: */ + ACUTEST_STATE_EXCLUDED = -1, + ACUTEST_STATE_SUCCESS = 0, + ACUTEST_STATE_FAILED = 1, + ACUTEST_STATE_SKIPPED = 2 +}; + +int acutest_check_(int cond, const char* file, int line, const char* fmt, ...); +void acutest_case_(const char* fmt, ...); +void acutest_message_(const char* fmt, ...); +void acutest_dump_(const char* title, const void* addr, size_t size); +void acutest_abort_(void) ACUTEST_ATTRIBUTE_(noreturn); +#ifdef __cplusplus + } /* extern "C" */ +#endif + +#ifndef TEST_NO_MAIN + +#include +#include +#include +#include +#include + +#if defined(unix) || defined(__unix__) || defined(__unix) || defined(__APPLE__) + #define ACUTEST_UNIX_ 1 + #include + #include + #include + #include + #include + #include + #include + + #if defined CLOCK_PROCESS_CPUTIME_ID && defined CLOCK_MONOTONIC + #define ACUTEST_HAS_POSIX_TIMER_ 1 + #endif +#endif + +#if defined(_gnu_linux_) || defined(__linux__) + #define ACUTEST_LINUX_ 1 + #include + #include +#endif + +#if defined(_WIN32) || defined(__WIN32__) || defined(__WINDOWS__) + #define ACUTEST_WIN_ 1 + #include + #include +#endif + +#if defined(__APPLE__) + #define ACUTEST_MACOS_ + #include + #include + #include + #include + #include +#endif + +#ifdef __cplusplus +#ifndef TEST_NO_EXCEPTIONS + #include +#endif +#endif + +#ifdef __has_include + #if __has_include() + #include + #endif +#endif + +/* Note our global private identifiers end with '_' to mitigate risk of clash + * with the unit tests implementation. */ + +#ifdef __cplusplus + extern "C" { +#endif + +#ifdef _MSC_VER + /* In the multi-platform code like ours, we cannot use the non-standard + * "safe" functions from Microsoft C lib like e.g. sprintf_s() instead of + * standard sprintf(). Hence, lets disable the warning C4996. */ + #pragma warning(push) + #pragma warning(disable: 4996) +#endif + + +struct acutest_test_ { + const char* name; + void (*func)(void); +}; + +struct acutest_test_data_ { + enum acutest_state_ state; + double duration; +}; + + +extern const struct acutest_test_ acutest_list_[]; + + +static char* acutest_argv0_ = NULL; +static int acutest_list_size_ = 0; +static struct acutest_test_data_* acutest_test_data_ = NULL; +static int acutest_no_exec_ = -1; +static int acutest_no_summary_ = 0; +static int acutest_tap_ = 0; +static int acutest_exclude_mode_ = 0; +static int acutest_worker_ = 0; +static int acutest_worker_index_ = 0; +static int acutest_cond_failed_ = 0; +static FILE *acutest_xml_output_ = NULL; + +static const struct acutest_test_* acutest_current_test_ = NULL; +static int acutest_current_index_ = 0; +static char acutest_case_name_[TEST_CASE_MAXSIZE] = ""; +static int acutest_test_check_count_ = 0; +static int acutest_test_skip_count_ = 0; +static char acutest_test_skip_reason_[256] = ""; +static int acutest_test_already_logged_ = 0; +static int acutest_case_already_logged_ = 0; +static int acutest_verbose_level_ = 2; +static int acutest_test_failures_ = 0; +static int acutest_colorize_ = 0; +static int acutest_timer_ = 0; + +static int acutest_abort_has_jmp_buf_ = 0; +static jmp_buf acutest_abort_jmp_buf_; + +static int +acutest_count_(enum acutest_state_ state) +{ + int i, n; + + for(i = 0, n = 0; i < acutest_list_size_; i++) { + if(acutest_test_data_[i].state == state) + n++; + } + + return n; +} + +static void +acutest_cleanup_(void) +{ + free((void*) acutest_test_data_); +} + +static void ACUTEST_ATTRIBUTE_(noreturn) +acutest_exit_(int exit_code) +{ + acutest_cleanup_(); + exit(exit_code); +} + + +#if defined ACUTEST_WIN_ + typedef LARGE_INTEGER acutest_timer_type_; + static LARGE_INTEGER acutest_timer_freq_; + static acutest_timer_type_ acutest_timer_start_; + static acutest_timer_type_ acutest_timer_end_; + + static void + acutest_timer_init_(void) + { + QueryPerformanceFrequency(´st_timer_freq_); + } + + static void + acutest_timer_get_time_(LARGE_INTEGER* ts) + { + QueryPerformanceCounter(ts); + } + + static double + acutest_timer_diff_(LARGE_INTEGER start, LARGE_INTEGER end) + { + double duration = (double)(end.QuadPart - start.QuadPart); + duration /= (double)acutest_timer_freq_.QuadPart; + return duration; + } + + static void + acutest_timer_print_diff_(void) + { + printf("%.6lf secs", acutest_timer_diff_(acutest_timer_start_, acutest_timer_end_)); + } +#elif defined ACUTEST_HAS_POSIX_TIMER_ + static clockid_t acutest_timer_id_; + typedef struct timespec acutest_timer_type_; + static acutest_timer_type_ acutest_timer_start_; + static acutest_timer_type_ acutest_timer_end_; + + static void + acutest_timer_init_(void) + { + if(acutest_timer_ == 1) + acutest_timer_id_ = CLOCK_MONOTONIC; + else if(acutest_timer_ == 2) + acutest_timer_id_ = CLOCK_PROCESS_CPUTIME_ID; + } + + static void + acutest_timer_get_time_(struct timespec* ts) + { + clock_gettime(acutest_timer_id_, ts); + } + + static double + acutest_timer_diff_(struct timespec start, struct timespec end) + { + return (double)(end.tv_sec - start.tv_sec) + (double)(end.tv_nsec - start.tv_nsec) / 1e9; + } + + static void + acutest_timer_print_diff_(void) + { + printf("%.6lf secs", + acutest_timer_diff_(acutest_timer_start_, acutest_timer_end_)); + } +#else + typedef int acutest_timer_type_; + static acutest_timer_type_ acutest_timer_start_; + static acutest_timer_type_ acutest_timer_end_; + + void + acutest_timer_init_(void) + {} + + static void + acutest_timer_get_time_(int* ts) + { + (void) ts; + } + + static double + acutest_timer_diff_(int start, int end) + { + (void) start; + (void) end; + return 0.0; + } + + static void + acutest_timer_print_diff_(void) + {} +#endif + +#define ACUTEST_COLOR_DEFAULT_ 0 +#define ACUTEST_COLOR_RED_ 1 +#define ACUTEST_COLOR_GREEN_ 2 +#define ACUTEST_COLOR_YELLOW_ 3 +#define ACUTEST_COLOR_DEFAULT_INTENSIVE_ 10 +#define ACUTEST_COLOR_RED_INTENSIVE_ 11 +#define ACUTEST_COLOR_GREEN_INTENSIVE_ 12 +#define ACUTEST_COLOR_YELLOW_INTENSIVE_ 13 + +static int ACUTEST_ATTRIBUTE_(format (printf, 2, 3)) +acutest_colored_printf_(int color, const char* fmt, ...) +{ + va_list args; + char buffer[256]; + int n; + + va_start(args, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, args); + va_end(args); + buffer[sizeof(buffer)-1] = '\0'; + + if(!acutest_colorize_) { + return printf("%s", buffer); + } + +#if defined ACUTEST_UNIX_ + { + const char* col_str; + switch(color) { + case ACUTEST_COLOR_RED_: col_str = "\033[0;31m"; break; + case ACUTEST_COLOR_GREEN_: col_str = "\033[0;32m"; break; + case ACUTEST_COLOR_YELLOW_: col_str = "\033[0;33m"; break; + case ACUTEST_COLOR_RED_INTENSIVE_: col_str = "\033[1;31m"; break; + case ACUTEST_COLOR_GREEN_INTENSIVE_: col_str = "\033[1;32m"; break; + case ACUTEST_COLOR_YELLOW_INTENSIVE_: col_str = "\033[1;33m"; break; + case ACUTEST_COLOR_DEFAULT_INTENSIVE_: col_str = "\033[1m"; break; + default: col_str = "\033[0m"; break; + } + printf("%s", col_str); + n = printf("%s", buffer); + printf("\033[0m"); + return n; + } +#elif defined ACUTEST_WIN_ + { + HANDLE h; + CONSOLE_SCREEN_BUFFER_INFO info; + WORD attr; + + h = GetStdHandle(STD_OUTPUT_HANDLE); + GetConsoleScreenBufferInfo(h, &info); + + switch(color) { + case ACUTEST_COLOR_RED_: attr = FOREGROUND_RED; break; + case ACUTEST_COLOR_GREEN_: attr = FOREGROUND_GREEN; break; + case ACUTEST_COLOR_YELLOW_: attr = FOREGROUND_RED | FOREGROUND_GREEN; break; + case ACUTEST_COLOR_RED_INTENSIVE_: attr = FOREGROUND_RED | FOREGROUND_INTENSITY; break; + case ACUTEST_COLOR_GREEN_INTENSIVE_: attr = FOREGROUND_GREEN | FOREGROUND_INTENSITY; break; + case ACUTEST_COLOR_DEFAULT_INTENSIVE_: attr = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY; break; + case ACUTEST_COLOR_YELLOW_INTENSIVE_: attr = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; break; + default: attr = 0; break; + } + if(attr != 0) + SetConsoleTextAttribute(h, attr); + n = printf("%s", buffer); + SetConsoleTextAttribute(h, info.wAttributes); + return n; + } +#else + n = printf("%s", buffer); + return n; +#endif +} + +static const char* +acutest_basename_(const char* path) +{ + const char* name; + + name = strrchr(path, '/'); + if(name != NULL) + name++; + else + name = path; + +#ifdef ACUTEST_WIN_ + { + const char* alt_name; + + alt_name = strrchr(path, '\\'); + if(alt_name != NULL) + alt_name++; + else + alt_name = path; + + if(alt_name > name) + name = alt_name; + } +#endif + + return name; +} + +static void +acutest_begin_test_line_(const struct acutest_test_* test) +{ + if(!acutest_tap_) { + if(acutest_verbose_level_ >= 3) { + acutest_colored_printf_(ACUTEST_COLOR_DEFAULT_INTENSIVE_, "Test %s:\n", test->name); + acutest_test_already_logged_++; + } else if(acutest_verbose_level_ >= 1) { + int n; + char spaces[48]; + + n = acutest_colored_printf_(ACUTEST_COLOR_DEFAULT_INTENSIVE_, "Test %s... ", test->name); + memset(spaces, ' ', sizeof(spaces)); + if(n < (int) sizeof(spaces)) + printf("%.*s", (int) sizeof(spaces) - n, spaces); + } else { + acutest_test_already_logged_ = 1; + } + } +} + +static void +acutest_finish_test_line_(enum acutest_state_ state) +{ + if(acutest_tap_) { + printf("%s %d - %s%s\n", + (state == ACUTEST_STATE_SUCCESS || state == ACUTEST_STATE_SKIPPED) ? "ok" : "not ok", + acutest_current_index_ + 1, + acutest_current_test_->name, + (state == ACUTEST_STATE_SKIPPED) ? " # SKIP" : ""); + + if(state == ACUTEST_STATE_SUCCESS && acutest_timer_) { + printf("# Duration: "); + acutest_timer_print_diff_(); + printf("\n"); + } + } else { + int color; + const char* str; + + switch(state) { + case ACUTEST_STATE_SUCCESS: color = ACUTEST_COLOR_GREEN_INTENSIVE_; str = "OK"; break; + case ACUTEST_STATE_SKIPPED: color = ACUTEST_COLOR_YELLOW_INTENSIVE_; str = "SKIPPED"; break; + case ACUTEST_STATE_FAILED: /* Fall through. */ + default: color = ACUTEST_COLOR_RED_INTENSIVE_; str = "FAILED"; break; + } + + printf("[ "); + acutest_colored_printf_(color, "%s", str); + printf(" ]"); + + if(state == ACUTEST_STATE_SUCCESS && acutest_timer_) { + printf(" "); + acutest_timer_print_diff_(); + } + + printf("\n"); + } +} + +static void +acutest_line_indent_(int level) +{ + static const char spaces[] = " "; + int n = level * 2; + + if(acutest_tap_ && n > 0) { + n--; + printf("#"); + } + + while(n > 16) { + printf("%s", spaces); + n -= 16; + } + printf("%.*s", n, spaces); +} + +void ACUTEST_ATTRIBUTE_(format (printf, 3, 4)) +acutest_skip_(const char* file, int line, const char* fmt, ...) +{ + va_list args; + size_t reason_len; + + va_start(args, fmt); + vsnprintf(acutest_test_skip_reason_, sizeof(acutest_test_skip_reason_), fmt, args); + va_end(args); + acutest_test_skip_reason_[sizeof(acutest_test_skip_reason_)-1] = '\0'; + + /* Remove final dot, if provided; that collides with our other logic. */ + reason_len = strlen(acutest_test_skip_reason_); + if(acutest_test_skip_reason_[reason_len-1] == '.') + acutest_test_skip_reason_[reason_len-1] = '\0'; + + if(acutest_test_check_count_ > 0) { + acutest_check_(0, file, line, "Cannot skip, already performed some checks"); + return; + } + + if(acutest_verbose_level_ >= 2) { + const char *result_str = "skipped"; + int result_color = ACUTEST_COLOR_YELLOW_; + + if(!acutest_test_already_logged_ && acutest_current_test_ != NULL) + acutest_finish_test_line_(ACUTEST_STATE_SKIPPED); + acutest_test_already_logged_++; + + acutest_line_indent_(1); + + if(file != NULL) { + file = acutest_basename_(file); + printf("%s:%d: ", file, line); + } + + printf("%s... ", acutest_test_skip_reason_); + acutest_colored_printf_(result_color, "%s", result_str); + printf("\n"); + acutest_test_already_logged_++; + } + + acutest_test_skip_count_++; +} + +int ACUTEST_ATTRIBUTE_(format (printf, 4, 5)) +acutest_check_(int cond, const char* file, int line, const char* fmt, ...) +{ + const char *result_str; + int result_color; + int verbose_level; + + if(acutest_test_skip_count_) { + /* We've skipped the test. We shouldn't be here: The test implementation + * should have already return before. So lets suppress the following + * output. */ + cond = 1; + goto skip_check; + } + + if(cond) { + result_str = "ok"; + result_color = ACUTEST_COLOR_GREEN_; + verbose_level = 3; + } else { + if(!acutest_test_already_logged_ && acutest_current_test_ != NULL) + acutest_finish_test_line_(ACUTEST_STATE_FAILED); + + acutest_test_failures_++; + acutest_test_already_logged_++; + + result_str = "failed"; + result_color = ACUTEST_COLOR_RED_; + verbose_level = 2; + } + + if(acutest_verbose_level_ >= verbose_level) { + va_list args; + + if(!acutest_case_already_logged_ && acutest_case_name_[0]) { + acutest_line_indent_(1); + acutest_colored_printf_(ACUTEST_COLOR_DEFAULT_INTENSIVE_, "Case %s:\n", acutest_case_name_); + acutest_test_already_logged_++; + acutest_case_already_logged_++; + } + + acutest_line_indent_(acutest_case_name_[0] ? 2 : 1); + if(file != NULL) { + file = acutest_basename_(file); + printf("%s:%d: ", file, line); + } + + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); + + printf("... "); + acutest_colored_printf_(result_color, "%s", result_str); + printf("\n"); + acutest_test_already_logged_++; + } + + acutest_test_check_count_++; + +skip_check: + acutest_cond_failed_ = (cond == 0); + return !acutest_cond_failed_; +} + +void ACUTEST_ATTRIBUTE_(format (printf, 1, 2)) +acutest_case_(const char* fmt, ...) +{ + va_list args; + + if(acutest_verbose_level_ < 2) + return; + + if(acutest_case_name_[0]) { + acutest_case_already_logged_ = 0; + acutest_case_name_[0] = '\0'; + } + + if(fmt == NULL) + return; + + va_start(args, fmt); + vsnprintf(acutest_case_name_, sizeof(acutest_case_name_) - 1, fmt, args); + va_end(args); + acutest_case_name_[sizeof(acutest_case_name_) - 1] = '\0'; + + if(acutest_verbose_level_ >= 3) { + acutest_line_indent_(1); + acutest_colored_printf_(ACUTEST_COLOR_DEFAULT_INTENSIVE_, "Case %s:\n", acutest_case_name_); + acutest_test_already_logged_++; + acutest_case_already_logged_++; + } +} + +void ACUTEST_ATTRIBUTE_(format (printf, 1, 2)) +acutest_message_(const char* fmt, ...) +{ + char buffer[TEST_MSG_MAXSIZE]; + char* line_beg; + char* line_end; + va_list args; + + if(acutest_verbose_level_ < 2) + return; + + /* We allow extra message only when something is already wrong in the + * current test. */ + if(acutest_current_test_ == NULL || !acutest_cond_failed_) + return; + + va_start(args, fmt); + vsnprintf(buffer, TEST_MSG_MAXSIZE, fmt, args); + va_end(args); + buffer[TEST_MSG_MAXSIZE-1] = '\0'; + + line_beg = buffer; + while(1) { + line_end = strchr(line_beg, '\n'); + if(line_end == NULL) + break; + acutest_line_indent_(acutest_case_name_[0] ? 3 : 2); + printf("%.*s\n", (int)(line_end - line_beg), line_beg); + line_beg = line_end + 1; + } + if(line_beg[0] != '\0') { + acutest_line_indent_(acutest_case_name_[0] ? 3 : 2); + printf("%s\n", line_beg); + } +} + +void +acutest_dump_(const char* title, const void* addr, size_t size) +{ + static const size_t BYTES_PER_LINE = 16; + size_t line_beg; + size_t truncate = 0; + + if(acutest_verbose_level_ < 2) + return; + + /* We allow extra message only when something is already wrong in the + * current test. */ + if(acutest_current_test_ == NULL || !acutest_cond_failed_) + return; + + if(size > TEST_DUMP_MAXSIZE) { + truncate = size - TEST_DUMP_MAXSIZE; + size = TEST_DUMP_MAXSIZE; + } + + acutest_line_indent_(acutest_case_name_[0] ? 3 : 2); + printf((title[strlen(title)-1] == ':') ? "%s\n" : "%s:\n", title); + + for(line_beg = 0; line_beg < size; line_beg += BYTES_PER_LINE) { + size_t line_end = line_beg + BYTES_PER_LINE; + size_t off; + + acutest_line_indent_(acutest_case_name_[0] ? 4 : 3); + printf("%08lx: ", (unsigned long)line_beg); + for(off = line_beg; off < line_end; off++) { + if(off < size) + printf(" %02x", ((const unsigned char*)addr)[off]); + else + printf(" "); + } + + printf(" "); + for(off = line_beg; off < line_end; off++) { + unsigned char byte = ((const unsigned char*)addr)[off]; + if(off < size) + printf("%c", (iscntrl(byte) ? '.' : byte)); + else + break; + } + + printf("\n"); + } + + if(truncate > 0) { + acutest_line_indent_(acutest_case_name_[0] ? 4 : 3); + printf(" ... (and more %u bytes)\n", (unsigned) truncate); + } +} + +/* This is called just before each test */ +static void +acutest_init_(const char *test_name) +{ +#ifdef TEST_INIT + TEST_INIT + ; /* Allow for a single unterminated function call */ +#endif + + /* Suppress any warnings about unused variable. */ + (void) test_name; +} + +/* This is called after each test */ +static void +acutest_fini_(const char *test_name) +{ +#ifdef TEST_FINI + TEST_FINI + ; /* Allow for a single unterminated function call */ +#endif + + /* Suppress any warnings about unused variable. */ + (void) test_name; +} + +void +acutest_abort_(void) +{ + if(acutest_abort_has_jmp_buf_) { + longjmp(acutest_abort_jmp_buf_, 1); + } else { + if(acutest_current_test_ != NULL) + acutest_fini_(acutest_current_test_->name); + fflush(stdout); + fflush(stderr); + acutest_exit_(ACUTEST_STATE_FAILED); + } +} + +static void +acutest_list_names_(void) +{ + const struct acutest_test_* test; + + printf("Unit tests:\n"); + for(test = ´st_list_[0]; test->func != NULL; test++) + printf(" %s\n", test->name); +} + +static int +acutest_name_contains_word_(const char* name, const char* pattern) +{ + static const char word_delim[] = " \t-_/.,:;"; + const char* substr; + size_t pattern_len; + + pattern_len = strlen(pattern); + + substr = strstr(name, pattern); + while(substr != NULL) { + int starts_on_word_boundary = (substr == name || strchr(word_delim, substr[-1]) != NULL); + int ends_on_word_boundary = (substr[pattern_len] == '\0' || strchr(word_delim, substr[pattern_len]) != NULL); + + if(starts_on_word_boundary && ends_on_word_boundary) + return 1; + + substr = strstr(substr+1, pattern); + } + + return 0; +} + +static int +acutest_select_(const char* pattern) +{ + int i; + int n = 0; + + /* Try exact match. */ + for(i = 0; i < acutest_list_size_; i++) { + if(strcmp(acutest_list_[i].name, pattern) == 0) { + acutest_test_data_[i].state = ACUTEST_STATE_SELECTED; + n++; + break; + } + } + if(n > 0) + return n; + + /* Try word match. */ + for(i = 0; i < acutest_list_size_; i++) { + if(acutest_name_contains_word_(acutest_list_[i].name, pattern)) { + acutest_test_data_[i].state = ACUTEST_STATE_SELECTED; + n++; + } + } + if(n > 0) + return n; + + /* Try relaxed match. */ + for(i = 0; i < acutest_list_size_; i++) { + if(strstr(acutest_list_[i].name, pattern) != NULL) { + acutest_test_data_[i].state = ACUTEST_STATE_SELECTED; + n++; + } + } + + return n; +} + + +/* Called if anything goes bad in Acutest, or if the unit test ends in other + * way then by normal returning from its function (e.g. exception or some + * abnormal child process termination). */ +static void ACUTEST_ATTRIBUTE_(format (printf, 1, 2)) +acutest_error_(const char* fmt, ...) +{ + if(acutest_verbose_level_ == 0) + return; + + if(acutest_verbose_level_ >= 2) { + va_list args; + + acutest_line_indent_(1); + if(acutest_verbose_level_ >= 3) + acutest_colored_printf_(ACUTEST_COLOR_RED_INTENSIVE_, "ERROR: "); + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); + printf("\n"); + } + + if(acutest_verbose_level_ >= 3) { + printf("\n"); + } +} + +/* Call directly the given test unit function. */ +static enum acutest_state_ +acutest_do_run_(const struct acutest_test_* test, int index) +{ + enum acutest_state_ state = ACUTEST_STATE_FAILED; + + acutest_current_test_ = test; + acutest_current_index_ = index; + acutest_test_failures_ = 0; + acutest_test_already_logged_ = 0; + acutest_test_check_count_ = 0; + acutest_test_skip_count_ = 0; + acutest_cond_failed_ = 0; + +#ifdef __cplusplus +#ifndef TEST_NO_EXCEPTIONS + try { +#endif +#endif + acutest_init_(test->name); + acutest_begin_test_line_(test); + + /* This is good to do in case the test unit crashes. */ + fflush(stdout); + fflush(stderr); + + if(!acutest_worker_) { + acutest_abort_has_jmp_buf_ = 1; + if(setjmp(acutest_abort_jmp_buf_) != 0) + goto aborted; + } + + acutest_timer_get_time_(´st_timer_start_); + test->func(); + +aborted: + acutest_abort_has_jmp_buf_ = 0; + acutest_timer_get_time_(´st_timer_end_); + + if(acutest_test_failures_ > 0) + state = ACUTEST_STATE_FAILED; + else if(acutest_test_skip_count_ > 0) + state = ACUTEST_STATE_SKIPPED; + else + state = ACUTEST_STATE_SUCCESS; + + if(!acutest_test_already_logged_) + acutest_finish_test_line_(state); + + if(acutest_verbose_level_ >= 3) { + acutest_line_indent_(1); + switch(state) { + case ACUTEST_STATE_SUCCESS: + acutest_colored_printf_(ACUTEST_COLOR_GREEN_INTENSIVE_, "SUCCESS: "); + printf("All conditions have passed.\n"); + + if(acutest_timer_) { + acutest_line_indent_(1); + printf("Duration: "); + acutest_timer_print_diff_(); + printf("\n"); + } + break; + + case ACUTEST_STATE_SKIPPED: + acutest_colored_printf_(ACUTEST_COLOR_YELLOW_INTENSIVE_, "SKIPPED: "); + printf("%s.\n", acutest_test_skip_reason_); + break; + + default: + acutest_colored_printf_(ACUTEST_COLOR_RED_INTENSIVE_, "FAILED: "); + printf("%d condition%s %s failed.\n", + acutest_test_failures_, + (acutest_test_failures_ == 1) ? "" : "s", + (acutest_test_failures_ == 1) ? "has" : "have"); + break; + } + printf("\n"); + } + +#ifdef __cplusplus +#ifndef TEST_NO_EXCEPTIONS + } catch(std::exception& e) { + const char* what = e.what(); + acutest_check_(0, NULL, 0, "Threw std::exception"); + if(what != NULL) + acutest_message_("std::exception::what(): %s", what); + + if(acutest_verbose_level_ >= 3) { + acutest_line_indent_(1); + acutest_colored_printf_(ACUTEST_COLOR_RED_INTENSIVE_, "FAILED: "); + printf("C++ exception.\n\n"); + } + } catch(...) { + acutest_check_(0, NULL, 0, "Threw an exception"); + + if(acutest_verbose_level_ >= 3) { + acutest_line_indent_(1); + acutest_colored_printf_(ACUTEST_COLOR_RED_INTENSIVE_, "FAILED: "); + printf("C++ exception.\n\n"); + } + } +#endif +#endif + + acutest_fini_(test->name); + acutest_case_(NULL); + acutest_current_test_ = NULL; + + return state; +} + +/* Trigger the unit test. If possible (and not suppressed) it starts a child + * process who calls acutest_do_run_(), otherwise it calls acutest_do_run_() + * directly. */ +static void +acutest_run_(const struct acutest_test_* test, int index, int master_index) +{ + enum acutest_state_ state = ACUTEST_STATE_FAILED; + acutest_timer_type_ start, end; + + acutest_current_test_ = test; + acutest_test_already_logged_ = 0; + acutest_timer_get_time_(&start); + + if(!acutest_no_exec_) { + +#if defined(ACUTEST_UNIX_) + + pid_t pid; + int exit_code; + + /* Make sure the child starts with empty I/O buffers. */ + fflush(stdout); + fflush(stderr); + + pid = fork(); + if(pid == (pid_t)-1) { + acutest_error_("Cannot fork. %s [%d]", strerror(errno), errno); + } else if(pid == 0) { + /* Child: Do the test. */ + acutest_worker_ = 1; + state = acutest_do_run_(test, index); + acutest_exit_((int) state); + } else { + /* Parent: Wait until child terminates and analyze its exit code. */ + waitpid(pid, &exit_code, 0); + if(WIFEXITED(exit_code)) { + state = (enum acutest_state_) WEXITSTATUS(exit_code); + } else if(WIFSIGNALED(exit_code)) { + char tmp[32]; + const char* signame; + switch(WTERMSIG(exit_code)) { + case SIGINT: signame = "SIGINT"; break; + case SIGHUP: signame = "SIGHUP"; break; + case SIGQUIT: signame = "SIGQUIT"; break; + case SIGABRT: signame = "SIGABRT"; break; + case SIGKILL: signame = "SIGKILL"; break; + case SIGSEGV: signame = "SIGSEGV"; break; + case SIGILL: signame = "SIGILL"; break; + case SIGTERM: signame = "SIGTERM"; break; + default: snprintf(tmp, sizeof(tmp), "signal %d", WTERMSIG(exit_code)); signame = tmp; break; + } + acutest_error_("Test interrupted by %s.", signame); + } else { + acutest_error_("Test ended in an unexpected way [%d].", exit_code); + } + } + +#elif defined(ACUTEST_WIN_) + + char buffer[512] = {0}; + STARTUPINFOA startupInfo; + PROCESS_INFORMATION processInfo; + DWORD exitCode; + + /* Windows has no fork(). So we propagate all info into the child + * through a command line arguments. */ + snprintf(buffer, sizeof(buffer), + "%s --worker=%d %s --no-exec --no-summary %s --verbose=%d --color=%s -- \"%s\"", + acutest_argv0_, index, acutest_timer_ ? "--time" : "", + acutest_tap_ ? "--tap" : "", acutest_verbose_level_, + acutest_colorize_ ? "always" : "never", + test->name); + memset(&startupInfo, 0, sizeof(startupInfo)); + startupInfo.cb = sizeof(STARTUPINFO); + if(CreateProcessA(NULL, buffer, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo)) { + WaitForSingleObject(processInfo.hProcess, INFINITE); + GetExitCodeProcess(processInfo.hProcess, &exitCode); + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); + switch(exitCode) { + case 0: state = ACUTEST_STATE_SUCCESS; break; + case 1: state = ACUTEST_STATE_FAILED; break; + case 2: state = ACUTEST_STATE_SKIPPED; break; + case 3: acutest_error_("Aborted."); break; + case 0xC0000005: acutest_error_("Access violation."); break; + default: acutest_error_("Test ended in an unexpected way [%lu].", exitCode); break; + } + } else { + acutest_error_("Cannot create unit test subprocess [%ld].", GetLastError()); + } + +#else + + /* A platform where we don't know how to run child process. */ + state = acutest_do_run_(test, index); + +#endif + + } else { + /* Child processes suppressed through --no-exec. */ + state = acutest_do_run_(test, index); + } + acutest_timer_get_time_(&end); + + acutest_current_test_ = NULL; + + acutest_test_data_[master_index].state = state; + acutest_test_data_[master_index].duration = acutest_timer_diff_(start, end); +} + +#if defined(ACUTEST_WIN_) +/* Callback for SEH events. */ +static LONG CALLBACK +acutest_seh_exception_filter_(EXCEPTION_POINTERS *ptrs) +{ + acutest_check_(0, NULL, 0, "Unhandled SEH exception"); + acutest_message_("Exception code: 0x%08lx", ptrs->ExceptionRecord->ExceptionCode); + acutest_message_("Exception address: 0x%p", ptrs->ExceptionRecord->ExceptionAddress); + + fflush(stdout); + fflush(stderr); + + return EXCEPTION_EXECUTE_HANDLER; +} +#endif + + +#define ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ 0x0001 +#define ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_ 0x0002 + +#define ACUTEST_CMDLINE_OPTID_NONE_ 0 +#define ACUTEST_CMDLINE_OPTID_UNKNOWN_ (-0x7fffffff + 0) +#define ACUTEST_CMDLINE_OPTID_MISSINGARG_ (-0x7fffffff + 1) +#define ACUTEST_CMDLINE_OPTID_BOGUSARG_ (-0x7fffffff + 2) + +typedef struct acutest_test_CMDLINE_OPTION_ { + char shortname; + const char* longname; + int id; + unsigned flags; +} ACUTEST_CMDLINE_OPTION_; + +static int +acutest_cmdline_handle_short_opt_group_(const ACUTEST_CMDLINE_OPTION_* options, + const char* arggroup, + int (*callback)(int /*optval*/, const char* /*arg*/)) +{ + const ACUTEST_CMDLINE_OPTION_* opt; + int i; + int ret = 0; + + for(i = 0; arggroup[i] != '\0'; i++) { + for(opt = options; opt->id != 0; opt++) { + if(arggroup[i] == opt->shortname) + break; + } + + if(opt->id != 0 && !(opt->flags & ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_)) { + ret = callback(opt->id, NULL); + } else { + /* Unknown option. */ + char badoptname[3]; + badoptname[0] = '-'; + badoptname[1] = arggroup[i]; + badoptname[2] = '\0'; + ret = callback((opt->id != 0 ? ACUTEST_CMDLINE_OPTID_MISSINGARG_ : ACUTEST_CMDLINE_OPTID_UNKNOWN_), + badoptname); + } + + if(ret != 0) + break; + } + + return ret; +} + +#define ACUTEST_CMDLINE_AUXBUF_SIZE_ 32 + +static int +acutest_cmdline_read_(const ACUTEST_CMDLINE_OPTION_* options, int argc, char** argv, + int (*callback)(int /*optval*/, const char* /*arg*/)) +{ + + const ACUTEST_CMDLINE_OPTION_* opt; + char auxbuf[ACUTEST_CMDLINE_AUXBUF_SIZE_+1]; + int after_doubledash = 0; + int i = 1; + int ret = 0; + + auxbuf[ACUTEST_CMDLINE_AUXBUF_SIZE_] = '\0'; + + while(i < argc) { + if(after_doubledash || strcmp(argv[i], "-") == 0) { + /* Non-option argument. */ + ret = callback(ACUTEST_CMDLINE_OPTID_NONE_, argv[i]); + } else if(strcmp(argv[i], "--") == 0) { + /* End of options. All the remaining members are non-option arguments. */ + after_doubledash = 1; + } else if(argv[i][0] != '-') { + /* Non-option argument. */ + ret = callback(ACUTEST_CMDLINE_OPTID_NONE_, argv[i]); + } else { + for(opt = options; opt->id != 0; opt++) { + if(opt->longname != NULL && strncmp(argv[i], "--", 2) == 0) { + size_t len = strlen(opt->longname); + if(strncmp(argv[i]+2, opt->longname, len) == 0) { + /* Regular long option. */ + if(argv[i][2+len] == '\0') { + /* with no argument provided. */ + if(!(opt->flags & ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_)) + ret = callback(opt->id, NULL); + else + ret = callback(ACUTEST_CMDLINE_OPTID_MISSINGARG_, argv[i]); + break; + } else if(argv[i][2+len] == '=') { + /* with an argument provided. */ + if(opt->flags & (ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ | ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_)) { + ret = callback(opt->id, argv[i]+2+len+1); + } else { + snprintf(auxbuf, sizeof(auxbuf), "--%s", opt->longname); + ret = callback(ACUTEST_CMDLINE_OPTID_BOGUSARG_, auxbuf); + } + break; + } else { + continue; + } + } + } else if(opt->shortname != '\0' && argv[i][0] == '-') { + if(argv[i][1] == opt->shortname) { + /* Regular short option. */ + if(opt->flags & ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_) { + if(argv[i][2] != '\0') + ret = callback(opt->id, argv[i]+2); + else if(i+1 < argc) + ret = callback(opt->id, argv[++i]); + else + ret = callback(ACUTEST_CMDLINE_OPTID_MISSINGARG_, argv[i]); + break; + } else { + ret = callback(opt->id, NULL); + + /* There might be more (argument-less) short options + * grouped together. */ + if(ret == 0 && argv[i][2] != '\0') + ret = acutest_cmdline_handle_short_opt_group_(options, argv[i]+2, callback); + break; + } + } + } + } + + if(opt->id == 0) { /* still not handled? */ + if(argv[i][0] != '-') { + /* Non-option argument. */ + ret = callback(ACUTEST_CMDLINE_OPTID_NONE_, argv[i]); + } else { + /* Unknown option. */ + char* badoptname = argv[i]; + + if(strncmp(badoptname, "--", 2) == 0) { + /* Strip any argument from the long option. */ + char* assignment = strchr(badoptname, '='); + if(assignment != NULL) { + size_t len = (size_t)(assignment - badoptname); + if(len > ACUTEST_CMDLINE_AUXBUF_SIZE_) + len = ACUTEST_CMDLINE_AUXBUF_SIZE_; + strncpy(auxbuf, badoptname, len); + auxbuf[len] = '\0'; + badoptname = auxbuf; + } + } + + ret = callback(ACUTEST_CMDLINE_OPTID_UNKNOWN_, badoptname); + } + } + } + + if(ret != 0) + return ret; + i++; + } + + return ret; +} + +static void +acutest_help_(void) +{ + printf("Usage: %s [options] [test...]\n", acutest_argv0_); + printf("\n"); + printf("Run the specified unit tests; or if the option '--exclude' is used, run all\n"); + printf("tests in the suite but those listed. By default, if no tests are specified\n"); + printf("on the command line, all unit tests in the suite are run.\n"); + printf("\n"); + printf("Options:\n"); + printf(" -X, --exclude Execute all unit tests but the listed ones\n"); + printf(" --exec[=WHEN] If supported, execute unit tests as child processes\n"); + printf(" (WHEN is one of 'auto', 'always', 'never')\n"); + printf(" -E, --no-exec Same as --exec=never\n"); +#if defined ACUTEST_WIN_ + printf(" -t, --time Measure test duration\n"); +#elif defined ACUTEST_HAS_POSIX_TIMER_ + printf(" -t, --time Measure test duration (real time)\n"); + printf(" --time=TIMER Measure test duration, using given timer\n"); + printf(" (TIMER is one of 'real', 'cpu')\n"); +#endif + printf(" --no-summary Suppress printing of test results summary\n"); + printf(" --tap Produce TAP-compliant output\n"); + printf(" (See https://testanything.org/)\n"); + printf(" -x, --xml-output=FILE Enable XUnit output to the given file\n"); + printf(" -l, --list List unit tests in the suite and exit\n"); + printf(" -v, --verbose Make output more verbose\n"); + printf(" --verbose=LEVEL Set verbose level to LEVEL:\n"); + printf(" 0 ... Be silent\n"); + printf(" 1 ... Output one line per test (and summary)\n"); + printf(" 2 ... As 1 and failed conditions (this is default)\n"); + printf(" 3 ... As 1 and all conditions (and extended summary)\n"); + printf(" -q, --quiet Same as --verbose=0\n"); + printf(" --color[=WHEN] Enable colorized output\n"); + printf(" (WHEN is one of 'auto', 'always', 'never')\n"); + printf(" --no-color Same as --color=never\n"); + printf(" -h, --help Display this help and exit\n"); + + if(acutest_list_size_ < 16) { + printf("\n"); + acutest_list_names_(); + } +} + +static const ACUTEST_CMDLINE_OPTION_ acutest_cmdline_options_[] = { + { 'X', "exclude", 'X', 0 }, + { 's', "skip", 'X', 0 }, /* kept for compatibility, use --exclude instead */ + { 0, "exec", 'e', ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ }, + { 'E', "no-exec", 'E', 0 }, +#if defined ACUTEST_WIN_ + { 't', "time", 't', 0 }, + { 0, "timer", 't', 0 }, /* kept for compatibility */ +#elif defined ACUTEST_HAS_POSIX_TIMER_ + { 't', "time", 't', ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ }, + { 0, "timer", 't', ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ }, /* kept for compatibility */ +#endif + { 0, "no-summary", 'S', 0 }, + { 0, "tap", 'T', 0 }, + { 'l', "list", 'l', 0 }, + { 'v', "verbose", 'v', ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ }, + { 'q', "quiet", 'q', 0 }, + { 0, "color", 'c', ACUTEST_CMDLINE_OPTFLAG_OPTIONALARG_ }, + { 0, "no-color", 'C', 0 }, + { 'h', "help", 'h', 0 }, + { 0, "worker", 'w', ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_ }, /* internal */ + { 'x', "xml-output", 'x', ACUTEST_CMDLINE_OPTFLAG_REQUIREDARG_ }, + { 0, NULL, 0, 0 } +}; + +static int +acutest_cmdline_callback_(int id, const char* arg) +{ + switch(id) { + case 'X': + acutest_exclude_mode_ = 1; + break; + + case 'e': + if(arg == NULL || strcmp(arg, "always") == 0) { + acutest_no_exec_ = 0; + } else if(strcmp(arg, "never") == 0) { + acutest_no_exec_ = 1; + } else if(strcmp(arg, "auto") == 0) { + /*noop*/ + } else { + fprintf(stderr, "%s: Unrecognized argument '%s' for option --exec.\n", acutest_argv0_, arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + } + break; + + case 'E': + acutest_no_exec_ = 1; + break; + + case 't': +#if defined ACUTEST_WIN_ || defined ACUTEST_HAS_POSIX_TIMER_ + if(arg == NULL || strcmp(arg, "real") == 0) { + acutest_timer_ = 1; + #ifndef ACUTEST_WIN_ + } else if(strcmp(arg, "cpu") == 0) { + acutest_timer_ = 2; + #endif + } else { + fprintf(stderr, "%s: Unrecognized argument '%s' for option --time.\n", acutest_argv0_, arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + } +#endif + break; + + case 'S': + acutest_no_summary_ = 1; + break; + + case 'T': + acutest_tap_ = 1; + break; + + case 'l': + acutest_list_names_(); + acutest_exit_(0); + break; + + case 'v': + acutest_verbose_level_ = (arg != NULL ? atoi(arg) : acutest_verbose_level_+1); + break; + + case 'q': + acutest_verbose_level_ = 0; + break; + + case 'c': + if(arg == NULL || strcmp(arg, "always") == 0) { + acutest_colorize_ = 1; + } else if(strcmp(arg, "never") == 0) { + acutest_colorize_ = 0; + } else if(strcmp(arg, "auto") == 0) { + /*noop*/ + } else { + fprintf(stderr, "%s: Unrecognized argument '%s' for option --color.\n", acutest_argv0_, arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + } + break; + + case 'C': + acutest_colorize_ = 0; + break; + + case 'h': + acutest_help_(); + acutest_exit_(0); + break; + + case 'w': + acutest_worker_ = 1; + acutest_worker_index_ = atoi(arg); + break; + case 'x': + acutest_xml_output_ = fopen(arg, "w"); + if (!acutest_xml_output_) { + fprintf(stderr, "Unable to open '%s': %s\n", arg, strerror(errno)); + acutest_exit_(2); + } + break; + + case 0: + if(acutest_select_(arg) == 0) { + fprintf(stderr, "%s: Unrecognized unit test '%s'\n", acutest_argv0_, arg); + fprintf(stderr, "Try '%s --list' for list of unit tests.\n", acutest_argv0_); + acutest_exit_(2); + } + break; + + case ACUTEST_CMDLINE_OPTID_UNKNOWN_: + fprintf(stderr, "Unrecognized command line option '%s'.\n", arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + break; + + case ACUTEST_CMDLINE_OPTID_MISSINGARG_: + fprintf(stderr, "The command line option '%s' requires an argument.\n", arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + break; + + case ACUTEST_CMDLINE_OPTID_BOGUSARG_: + fprintf(stderr, "The command line option '%s' does not expect an argument.\n", arg); + fprintf(stderr, "Try '%s --help' for more information.\n", acutest_argv0_); + acutest_exit_(2); + break; + } + + return 0; +} + +static int +acutest_under_debugger_(void) +{ +#ifdef ACUTEST_LINUX_ + /* Scan /proc/self/status for line "TracerPid: [PID]". If such line exists + * and the PID is non-zero, we're being debugged. */ + { + static const int OVERLAP = 32; + int fd; + char buf[512]; + size_t n_read; + pid_t tracer_pid = 0; + + /* Little trick so that we can treat the 1st line the same as any other + * and detect line start easily. */ + buf[0] = '\n'; + n_read = 1; + + fd = open("/proc/self/status", O_RDONLY); + if(fd != -1) { + while(1) { + static const char pattern[] = "\nTracerPid:"; + const char* field; + + while(n_read < sizeof(buf) - 1) { + ssize_t n; + + n = read(fd, buf + n_read, sizeof(buf) - 1 - n_read); + if(n <= 0) + break; + n_read += (size_t)n; + } + buf[n_read] = '\0'; + + field = strstr(buf, pattern); + if(field != NULL && field < buf + sizeof(buf) - OVERLAP) { + tracer_pid = (pid_t) atoi(field + sizeof(pattern) - 1); + break; + } + + if(n_read == sizeof(buf) - 1) { + /* Move the tail with the potentially incomplete line we're + * be looking for to the beginning of the buffer. + * (The OVERLAP must be large enough so the searched line + * can fit in completely.) */ + memmove(buf, buf + sizeof(buf) - 1 - OVERLAP, OVERLAP); + n_read = OVERLAP; + } else { + break; + } + } + + close(fd); + + if(tracer_pid != 0) + return 1; + } + } +#endif + +#ifdef ACUTEST_MACOS_ + /* See https://developer.apple.com/library/archive/qa/qa1361/_index.html */ + { + int mib[4]; + struct kinfo_proc info; + size_t size; + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + + size = sizeof(info); + info.kp_proc.p_flag = 0; + sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); + + if(info.kp_proc.p_flag & P_TRACED) + return 1; + } +#endif + +#ifdef ACUTEST_WIN_ + if(IsDebuggerPresent()) + return 1; +#endif + +#ifdef RUNNING_ON_VALGRIND + /* We treat Valgrind as a debugger of sorts. + * (Macro RUNNING_ON_VALGRIND is provided by , if available.) */ + if(RUNNING_ON_VALGRIND) + return 1; +#endif + + return 0; +} + +int +main(int argc, char** argv) +{ + int i, index; + int exit_code = 1; + + acutest_argv0_ = argv[0]; + +#if defined ACUTEST_UNIX_ + acutest_colorize_ = isatty(STDOUT_FILENO); +#elif defined ACUTEST_WIN_ + #if defined _BORLANDC_ + acutest_colorize_ = isatty(_fileno(stdout)); + #else + acutest_colorize_ = _isatty(_fileno(stdout)); + #endif +#else + acutest_colorize_ = 0; +#endif + + /* Count all test units */ + acutest_list_size_ = 0; + for(i = 0; acutest_list_[i].func != NULL; i++) + acutest_list_size_++; + + acutest_test_data_ = (struct acutest_test_data_*)calloc(acutest_list_size_, sizeof(struct acutest_test_data_)); + if(acutest_test_data_ == NULL) { + fprintf(stderr, "Out of memory.\n"); + acutest_exit_(2); + } + + /* Parse options */ + acutest_cmdline_read_(acutest_cmdline_options_, argc, argv, acutest_cmdline_callback_); + + /* Initialize the proper timer. */ + acutest_timer_init_(); + +#if defined(ACUTEST_WIN_) + SetUnhandledExceptionFilter(acutest_seh_exception_filter_); +#ifdef _MSC_VER + _set_abort_behavior(0, _WRITE_ABORT_MSG); +#endif +#endif + + /* Determine what to run. */ + if(acutest_count_(ACUTEST_STATE_SELECTED) > 0) { + enum acutest_state_ if_selected; + enum acutest_state_ if_unselected; + + if(!acutest_exclude_mode_) { + if_selected = ACUTEST_STATE_NEEDTORUN; + if_unselected = ACUTEST_STATE_EXCLUDED; + } else { + if_selected = ACUTEST_STATE_EXCLUDED; + if_unselected = ACUTEST_STATE_NEEDTORUN; + } + + for(i = 0; acutest_list_[i].func != NULL; i++) { + if(acutest_test_data_[i].state == ACUTEST_STATE_SELECTED) + acutest_test_data_[i].state = if_selected; + else + acutest_test_data_[i].state = if_unselected; + } + } else { + /* By default, we want to run all tests. */ + for(i = 0; acutest_list_[i].func != NULL; i++) + acutest_test_data_[i].state = ACUTEST_STATE_NEEDTORUN; + } + + /* By default, we want to suppress running tests as child processes if we + * run just one test, or if we're under debugger: Debugging tests is then + * so much easier. */ + if(acutest_no_exec_ < 0) { + if(acutest_count_(ACUTEST_STATE_NEEDTORUN) <= 1 || acutest_under_debugger_()) + acutest_no_exec_ = 1; + else + acutest_no_exec_ = 0; + } + + if(acutest_tap_) { + /* TAP requires we know test result ("ok", "not ok") before we output + * anything about the test, and this gets problematic for larger verbose + * levels. */ + if(acutest_verbose_level_ > 2) + acutest_verbose_level_ = 2; + + /* TAP harness should provide some summary. */ + acutest_no_summary_ = 1; + + if(!acutest_worker_) + printf("1..%d\n", acutest_count_(ACUTEST_STATE_NEEDTORUN)); + } + + index = acutest_worker_index_; + for(i = 0; acutest_list_[i].func != NULL; i++) { + if(acutest_test_data_[i].state == ACUTEST_STATE_NEEDTORUN) + acutest_run_(´st_list_[i], index++, i); + } + + /* Write a summary */ + if(!acutest_no_summary_ && acutest_verbose_level_ >= 1) { + int n_run, n_success, n_failed ; + + n_run = acutest_list_size_ - acutest_count_(ACUTEST_STATE_EXCLUDED); + n_success = acutest_count_(ACUTEST_STATE_SUCCESS); + n_failed = acutest_count_(ACUTEST_STATE_FAILED); + + if(acutest_verbose_level_ >= 3) { + acutest_colored_printf_(ACUTEST_COLOR_DEFAULT_INTENSIVE_, "Summary:\n"); + + printf(" Count of run unit tests: %4d\n", n_run); + printf(" Count of successful unit tests: %4d\n", n_success); + printf(" Count of failed unit tests: %4d\n", n_failed); + } + + if(n_failed == 0) { + acutest_colored_printf_(ACUTEST_COLOR_GREEN_INTENSIVE_, "SUCCESS:"); + printf(" No unit tests have failed.\n"); + } else { + acutest_colored_printf_(ACUTEST_COLOR_RED_INTENSIVE_, "FAILED:"); + printf(" %d of %d unit tests %s failed.\n", + n_failed, n_run, (n_failed == 1) ? "has" : "have"); + } + + if(acutest_verbose_level_ >= 3) + printf("\n"); + } + + if (acutest_xml_output_) { + const char* suite_name = acutest_basename_(argv[0]); + fprintf(acutest_xml_output_, "\n"); + fprintf(acutest_xml_output_, "\n", + suite_name, + (int)acutest_list_size_, + acutest_count_(ACUTEST_STATE_FAILED), + acutest_count_(ACUTEST_STATE_SKIPPED) + acutest_count_(ACUTEST_STATE_EXCLUDED)); + for(i = 0; acutest_list_[i].func != NULL; i++) { + struct acutest_test_data_ *details = ´st_test_data_[i]; + const char* str_state; + fprintf(acutest_xml_output_, " \n", acutest_list_[i].name, details->duration); + + switch(details->state) { + case ACUTEST_STATE_SUCCESS: str_state = NULL; break; + case ACUTEST_STATE_EXCLUDED: /* Fall through. */ + case ACUTEST_STATE_SKIPPED: str_state = ""; break; + case ACUTEST_STATE_FAILED: /* Fall through. */ + default: str_state = ""; break; + } + + if(str_state != NULL) + fprintf(acutest_xml_output_, " %s\n", str_state); + fprintf(acutest_xml_output_, " \n"); + } + fprintf(acutest_xml_output_, "\n"); + fclose(acutest_xml_output_); + } + + if(acutest_worker_ && acutest_count_(ACUTEST_STATE_EXCLUDED)+1 == acutest_list_size_) { + /* If we are the child process, we need to propagate the test state + * without any moderation. */ + for(i = 0; acutest_list_[i].func != NULL; i++) { + if(acutest_test_data_[i].state != ACUTEST_STATE_EXCLUDED) { + exit_code = (int) acutest_test_data_[i].state; + break; + } + } + } else { + if(acutest_count_(ACUTEST_STATE_FAILED) > 0) + exit_code = 1; + else + exit_code = 0; + } + + acutest_cleanup_(); + return exit_code; +} + + +#endif /* #ifndef TEST_NO_MAIN */ + +#ifdef _MSC_VER + #pragma warning(pop) +#endif + +#ifdef __cplusplus + } /* extern "C" */ +#endif + +#endif /* #ifndef ACUTEST_H */ diff --git a/src/main.c b/src/main.c index 279db4e..13a8d16 100644 --- a/src/main.c +++ b/src/main.c @@ -20,28 +20,28 @@ static void print_help(void) { "{h1}try{reset} v" TRY_VERSION " - ephemeral workspace manager\n\n" "To use try, add to your shell config:\n\n" " {dim}# bash/zsh (~/.bashrc or ~/.zshrc){reset}\n" - " {b}eval \"$(try init ~/src/tries)\"{/b}\n\n" + " {highlight}eval \"$(try init ~/src/tries)\"{/}\n\n" " {dim}# fish (~/.config/fish/config.fish){reset}\n" - " {b}eval (try init ~/src/tries | string collect){/b}\n\n" + " {highlight}eval (try init ~/src/tries | string collect){/}\n\n" "Then use:\n" - " {b}try{/b} [query] Interactive directory selector\n" - " {b}try clone{/b} Clone repo into dated directory\n" - " {b}try worktree{/b} Create worktree from current git repo\n" - " {b}try --help{/b} Show this help\n" - " {b}try --no-colors{/b} Disable colors in output\n\n" + " {b}try{/} [query] Interactive directory selector\n" + " {b}try clone{/} Clone repo into dated directory\n" + " {b}try worktree{/} Create worktree from current git repo\n" + " {b}try --help{/} Show this help\n" + " {b}try --no-colors{/} Disable colors in output\n\n" "{dim}Manual mode (without alias):{reset}\n" - " {b}try exec{/b} [query] Output shell script to eval\n\n" + " {b}try exec{/} [query] Output shell script to eval\n\n" "{dim}Defaults:{reset}\n" - " Default path: {dim}~/src/tries{reset} (override with {b}--path{/b} on init)\n" + " Default path: {dim}~/src/tries{reset} (override with {b}--path{/} on init)\n" " Current default: {dim}"); zstr_cat(&help, zstr_cstr(&default_path)); zstr_cat(&help, "{reset}\n\n" "{dim}Clone Examples:{reset}\n" - " {b}try clone https://github.com/tobi/try.git{/b}\n" + " {highlight}try clone https://github.com/tobi/try.git{/}\n" " {dim}# Creates: 2025-08-27-tobi-try{reset}\n\n" - " {b}try clone https://github.com/tobi/try.git my-fork{/b}\n" + " {highlight}try clone https://github.com/tobi/try.git my-fork{/}\n" " {dim}# Creates: my-fork{reset}\n\n" - " {b}try https://github.com/tobi/try.git{/b}\n" + " {highlight}try https://github.com/tobi/try.git{/}\n" " {dim}# Shorthand for clone (same as first example){reset}\n"); Z_CLEANUP(zstr_free) zstr expanded = zstr_expand_tokens(zstr_cstr(&help)); diff --git a/src/tokens.c b/src/tokens.c new file mode 100644 index 0000000..a6ae6d2 --- /dev/null +++ b/src/tokens.c @@ -0,0 +1,3902 @@ + +#line 1 "src/tokens.rl" +/* + * Token expansion state machine - Ragel grammar + * + * This provides a stack-based token system for ANSI escape code generation. + * Tokens like {b}, {dim}, {red} push state, and {/} pops to restore. + * + * Generated with: ragel -C -G2 tokens.rl -o tokens.c + */ + +#include "tokens.h" +#include "zstr.h" +#include +#include +#include + +/* Maximum nesting depth for style stack */ +#define MAX_STACK_DEPTH 32 + +/* + * Global style configuration for semantic tokens. + * These can be overridden at runtime to customize the appearance. + * Format: ANSI escape sequence string (without the leading \033[) + */ +const char *token_style_h1 = "1m\033[38;5;214"; /* Bold + orange (256-color 214) */ +const char *token_style_h2 = "1;34"; /* Bold + blue */ +const char *token_style_h3 = "1;37"; /* Bold + white */ +const char *token_style_h4 = "1;37"; /* Bold + white */ +const char *token_style_h5 = "1;37"; /* Bold + white */ +const char *token_style_h6 = "1;37"; /* Bold + white */ +const char *token_style_b = "1"; /* Bold (same as strong) */ +const char *token_style_strong = "1"; /* Bold */ +const char *token_style_highlight = "1;33"; /* Bold + yellow */ +const char *token_style_dim = "90"; /* Gray (bright black) */ +const char *token_style_danger = "48;5;52"; /* Dark red bg */ + +/* Style attribute types */ +typedef enum { + ATTR_NONE = 0, + ATTR_BOLD, + ATTR_DIM, + ATTR_ITALIC, + ATTR_UNDERLINE, + ATTR_REVERSE, + ATTR_STRIKETHROUGH, + ATTR_FG, /* Foreground color */ + ATTR_BG, /* Background color */ + ATTR_COMPOSITE, /* Multiple attributes (for semantic tokens) */ +} AttrType; + +/* Represents a single attribute value */ +typedef struct { + AttrType type; + int value; /* For colors: ANSI code, for bools: 0/1 */ +} AttrValue; + +/* Stack entry - stores what to restore when popping */ +typedef struct { + AttrType type; + int prev_value; + int count; /* For composite: how many individual attrs were pushed */ +} StackEntry; + +/* Parser state */ +typedef struct { + zstr *out; /* Output buffer */ + StackEntry stack[MAX_STACK_DEPTH]; + int stack_depth; + int cursor_col; /* Visual column of {cursor} (1-indexed), -1 if no cursor */ + int cursor_row; /* Visual row of {cursor} (1-indexed), -1 if no cursor */ + bool has_cursor; /* True if {cursor} was encountered */ + int visual_col; /* Current visual column */ + int visual_row; /* Current visual row */ + bool no_colors; /* If true, don't emit ANSI codes */ + bool disabled; /* If true, pass through without expansion */ + + /* Desired style state (what tokens request) */ + int fg_color; /* 0 = default, else ANSI code */ + int bg_color; + bool bold; + bool dim; + bool italic; + bool underline; + bool reverse; + bool strikethrough; + + /* Emitted style state (what terminal currently has) */ + int emitted_fg; + int emitted_bg; + bool emitted_bold; + bool emitted_dim; + bool emitted_italic; + bool emitted_underline; + bool emitted_reverse; + bool emitted_strikethrough; + bool dirty; /* True if desired != emitted */ +} TokenParser; + +/* ANSI escape code helpers */ +static void emit_ansi(TokenParser *p, const char *code) { + if (!p->no_colors) { + zstr_cat(p->out, code); + } +} + +static void emit_ansi_num(TokenParser *p, const char *prefix, int n, const char *suffix) { + if (!p->no_colors) { + char buf[32]; + snprintf(buf, sizeof(buf), "%s%d%s", prefix, n, suffix); + zstr_cat(p->out, buf); + } +} + +/* Mark state as dirty (deferred emission) */ +static void mark_dirty(TokenParser *p) { + p->dirty = true; +} + +/* Sync emitted state with desired state - emit codes only when outputting a character */ +static void sync_styles(TokenParser *p) { + if (!p->dirty || p->no_colors) return; + + /* Check if we need a full reset (going back to default state) */ + bool need_reset = false; + + /* If any attribute is turning OFF, we need reset then re-apply actives */ + if ((p->emitted_bold && !p->bold) || + (p->emitted_dim && !p->dim) || + (p->emitted_italic && !p->italic) || + (p->emitted_underline && !p->underline) || + (p->emitted_reverse && !p->reverse) || + (p->emitted_strikethrough && !p->strikethrough) || + (p->emitted_fg != 0 && p->fg_color == 0) || + (p->emitted_bg != 0 && p->bg_color == 0)) { + need_reset = true; + } + + if (need_reset) { + /* Reset and re-apply all active styles */ + zstr_cat(p->out, "\033[0"); + if (p->bold) zstr_cat(p->out, ";1"); + if (p->dim) zstr_cat(p->out, ";2"); + if (p->italic) zstr_cat(p->out, ";3"); + if (p->underline) zstr_cat(p->out, ";4"); + if (p->reverse) zstr_cat(p->out, ";7"); + if (p->strikethrough) zstr_cat(p->out, ";9"); + if (p->fg_color != 0) { + char buf[20]; + if (p->fg_color >= 1000 && p->fg_color < 2000) { + /* 256-color foreground */ + snprintf(buf, sizeof(buf), ";38;5;%d", p->fg_color - 1000); + } else { + snprintf(buf, sizeof(buf), ";%d", p->fg_color); + } + zstr_cat(p->out, buf); + } + if (p->bg_color != 0) { + char buf[20]; + if (p->bg_color >= 2000) { + /* 256-color background */ + snprintf(buf, sizeof(buf), ";48;5;%d", p->bg_color - 2000); + } else { + snprintf(buf, sizeof(buf), ";%d", p->bg_color); + } + zstr_cat(p->out, buf); + } + zstr_cat(p->out, "m"); + } else { + /* Apply only changed attributes (turning ON) */ + bool first = true; + char buf[64]; + buf[0] = '\0'; + + if (p->bold && !p->emitted_bold) { + strcat(buf, first ? "1" : ";1"); first = false; + } + if (p->dim && !p->emitted_dim) { + strcat(buf, first ? "2" : ";2"); first = false; + } + if (p->italic && !p->emitted_italic) { + strcat(buf, first ? "3" : ";3"); first = false; + } + if (p->underline && !p->emitted_underline) { + strcat(buf, first ? "4" : ";4"); first = false; + } + if (p->reverse && !p->emitted_reverse) { + strcat(buf, first ? "7" : ";7"); first = false; + } + if (p->strikethrough && !p->emitted_strikethrough) { + strcat(buf, first ? "9" : ";9"); first = false; + } + if (p->fg_color != p->emitted_fg && p->fg_color != 0) { + char tmp[20]; + if (p->fg_color >= 1000 && p->fg_color < 2000) { + snprintf(tmp, sizeof(tmp), "%s38;5;%d", first ? "" : ";", p->fg_color - 1000); + } else { + snprintf(tmp, sizeof(tmp), "%s%d", first ? "" : ";", p->fg_color); + } + strcat(buf, tmp); first = false; + } + if (p->bg_color != p->emitted_bg && p->bg_color != 0) { + char tmp[20]; + if (p->bg_color >= 2000) { + snprintf(tmp, sizeof(tmp), "%s48;5;%d", first ? "" : ";", p->bg_color - 2000); + } else { + snprintf(tmp, sizeof(tmp), "%s%d", first ? "" : ";", p->bg_color); + } + strcat(buf, tmp); first = false; + } + + if (!first) { + zstr_cat(p->out, "\033["); + zstr_cat(p->out, buf); + zstr_cat(p->out, "m"); + } + } + + /* Update emitted state to match desired */ + p->emitted_bold = p->bold; + p->emitted_dim = p->dim; + p->emitted_italic = p->italic; + p->emitted_underline = p->underline; + p->emitted_reverse = p->reverse; + p->emitted_strikethrough = p->strikethrough; + p->emitted_fg = p->fg_color; + p->emitted_bg = p->bg_color; + p->dirty = false; +} + +/* Push a restore entry onto the stack */ +static void push_attr(TokenParser *p, AttrType type, int prev_value) { + if (p->stack_depth < MAX_STACK_DEPTH) { + p->stack[p->stack_depth].type = type; + p->stack[p->stack_depth].prev_value = prev_value; + p->stack[p->stack_depth].count = 1; + p->stack_depth++; + } +} + +/* Push a composite (multiple attrs at once) */ +static void push_composite(TokenParser *p, int count) { + if (p->stack_depth < MAX_STACK_DEPTH) { + p->stack[p->stack_depth].type = ATTR_COMPOSITE; + p->stack[p->stack_depth].prev_value = 0; + p->stack[p->stack_depth].count = count; + p->stack_depth++; + } +} + +/* Restore an attribute to its previous value (deferred - just updates state) */ +static void restore_attr(TokenParser *p, AttrType type, int prev_value) { + switch (type) { + case ATTR_BOLD: + p->bold = prev_value; + break; + case ATTR_DIM: + p->dim = prev_value; + break; + case ATTR_ITALIC: + p->italic = prev_value; + break; + case ATTR_UNDERLINE: + p->underline = prev_value; + break; + case ATTR_REVERSE: + p->reverse = prev_value; + break; + case ATTR_STRIKETHROUGH: + p->strikethrough = prev_value; + break; + case ATTR_FG: + p->fg_color = prev_value; + break; + case ATTR_BG: + p->bg_color = prev_value; + break; + default: + break; + } +} + +/* Pop one entry from the stack and restore */ +static void pop_style(TokenParser *p) { + if (p->stack_depth > 0) { + p->stack_depth--; + StackEntry *e = &p->stack[p->stack_depth]; + + if (e->type == ATTR_COMPOSITE) { + /* Pop multiple individual entries */ + for (int i = 0; i < e->count && p->stack_depth > 0; i++) { + p->stack_depth--; + StackEntry *inner = &p->stack[p->stack_depth]; + restore_attr(p, inner->type, inner->prev_value); + } + } else { + restore_attr(p, e->type, e->prev_value); + } + mark_dirty(p); + } +} + +/* Check if any styles are active */ +static bool has_active_styles(TokenParser *p) { + return p->bold || p->dim || p->italic || p->underline || + p->reverse || p->strikethrough || p->fg_color != 0 || p->bg_color != 0; +} + +/* Reset all styles and clear stack (deferred - just updates state) */ +static void reset_all(TokenParser *p) { + p->fg_color = 0; + p->bg_color = 0; + p->bold = false; + p->dim = false; + p->italic = false; + p->underline = false; + p->reverse = false; + p->strikethrough = false; + p->stack_depth = 0; + mark_dirty(p); +} + +/* Reset all styles at newline - emits immediately since newline follows */ +static void reset_line_styles(TokenParser *p) { + /* Check if emitted state has any active styles */ + if (p->emitted_bold || p->emitted_dim || p->emitted_italic || + p->emitted_underline || p->emitted_reverse || p->emitted_strikethrough || + p->emitted_fg != 0 || p->emitted_bg != 0) { + emit_ansi(p, "\033[0m"); + /* Update emitted state */ + p->emitted_bold = false; + p->emitted_dim = false; + p->emitted_italic = false; + p->emitted_underline = false; + p->emitted_reverse = false; + p->emitted_strikethrough = false; + p->emitted_fg = 0; + p->emitted_bg = 0; + } + /* Reset desired state too */ + p->fg_color = 0; + p->bg_color = 0; + p->bold = false; + p->dim = false; + p->italic = false; + p->underline = false; + p->reverse = false; + p->strikethrough = false; + p->dirty = false; + /* Keep stack_depth as-is so {/} still works across lines if needed */ +} + +/* Style application functions - deferred emission (just update state) */ +static void apply_bold(TokenParser *p) { + push_attr(p, ATTR_BOLD, p->bold); + p->bold = true; + mark_dirty(p); +} + +static void apply_dim(TokenParser *p) { + push_attr(p, ATTR_DIM, p->dim); + p->dim = true; + mark_dirty(p); +} + +static void apply_italic(TokenParser *p) { + push_attr(p, ATTR_ITALIC, p->italic); + p->italic = true; + mark_dirty(p); +} + +static void apply_underline(TokenParser *p) { + push_attr(p, ATTR_UNDERLINE, p->underline); + p->underline = true; + mark_dirty(p); +} + +static void apply_reverse(TokenParser *p) { + push_attr(p, ATTR_REVERSE, p->reverse); + p->reverse = true; + mark_dirty(p); +} + +static void apply_strikethrough(TokenParser *p) { + push_attr(p, ATTR_STRIKETHROUGH, p->strikethrough); + p->strikethrough = true; + mark_dirty(p); +} + +static void apply_fg_code(TokenParser *p, int code) { + push_attr(p, ATTR_FG, p->fg_color); + p->fg_color = code; + mark_dirty(p); +} + +static void apply_fg_256(TokenParser *p, int n) { + push_attr(p, ATTR_FG, p->fg_color); + p->fg_color = 1000 + n; /* Marker for 256-color */ + mark_dirty(p); +} + +static void apply_bg_code(TokenParser *p, int code) { + push_attr(p, ATTR_BG, p->bg_color); + p->bg_color = code; + mark_dirty(p); +} + +static void apply_bg_256(TokenParser *p, int n) { + push_attr(p, ATTR_BG, p->bg_color); + p->bg_color = 2000 + n; /* Marker for 256-color bg */ + mark_dirty(p); +} + +/* Semantic token applications - deferred emission (just update state) */ +static void apply_token_b(TokenParser *p) { + /* {b} = bold only (same as strong) */ + push_attr(p, ATTR_BOLD, p->bold); + p->bold = true; + mark_dirty(p); +} + +static void apply_token_highlight(TokenParser *p) { + /* {highlight} = bold + yellow */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 33; /* Yellow */ + mark_dirty(p); +} + +static void apply_token_h1(TokenParser *p) { + /* {h1} = bold + orange (256-color 214) */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 1214; /* 256-color marker for 214 */ + mark_dirty(p); +} + +static void apply_token_h2(TokenParser *p) { + /* {h2} = bold + blue */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 34; /* Blue */ + mark_dirty(p); +} + +static void apply_token_h3(TokenParser *p) { + /* {h3} = bold + white */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 37; /* White */ + mark_dirty(p); +} + +static void apply_token_h4(TokenParser *p) { + /* {h4} = bold + white */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 37; + mark_dirty(p); +} + +static void apply_token_h5(TokenParser *p) { + /* {h5} = bold + white */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 37; + mark_dirty(p); +} + +static void apply_token_h6(TokenParser *p) { + /* {h6} = bold + white */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 37; + mark_dirty(p); +} + +static void apply_token_strong(TokenParser *p) { + /* {strong} = bold */ + push_attr(p, ATTR_BOLD, p->bold); + p->bold = true; + mark_dirty(p); +} + +static void apply_token_dim_style(TokenParser *p) { + /* {dim} = gray (bright black) */ + push_attr(p, ATTR_FG, p->fg_color); + p->fg_color = 90; /* Bright black */ + mark_dirty(p); +} + +static void apply_token_section(TokenParser *p) { + /* {section} = bold + subtle dark gray background (256-color 237) */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_BG, p->bg_color); + push_composite(p, 2); + p->bold = true; + p->bg_color = 2237; /* 256-color marker for 237 */ + mark_dirty(p); +} + +static void apply_token_danger(TokenParser *p) { + /* {danger} = dark red background (256-color 52) */ + push_attr(p, ATTR_BG, p->bg_color); + p->bg_color = 2052; /* 256-color marker for 52 */ + mark_dirty(p); +} + +static void apply_token_text(TokenParser *p) { + /* {text} = full reset */ + reset_all(p); +} + +/* Brighten current foreground color */ +static void apply_bright(TokenParser *p) { + push_attr(p, ATTR_FG, p->fg_color); + /* If current color is 30-37 (standard), convert to 90-97 (bright) */ + if (p->fg_color >= 30 && p->fg_color <= 37) { + p->fg_color = p->fg_color + 60; + } else { + /* Already bright or custom - just set bright white */ + p->fg_color = 97; + } + mark_dirty(p); +} + +/* Parse a number from string, returns -1 on failure */ +static int parse_number(const char *start, const char *end) { + int n = 0; + for (const char *p = start; p < end; p++) { + if (*p < '0' || *p > '9') return -1; + n = n * 10 + (*p - '0'); + } + return n; +} + + +#line 553 "src/tokens.c" +static const int token_parser_start = 242; +static const int token_parser_first_final = 242; +static const int token_parser_error = 0; + +static const int token_parser_en_main = 242; + + +#line 887 "src/tokens.rl" + + +/* Global flags */ +bool zstr_disable_token_expansion = false; +bool zstr_no_colors = false; + +TokenExpansion expand_tokens_with_cursor(const char *text) { + TokenExpansion result = {0}; + result.expanded = zstr_init(); + result.cursor_col = -1; + result.cursor_row = -1; + result.has_cursor = false; + result.final_col = 1; + result.final_row = 1; + + if (!text || !*text) { + return result; + } + + /* If expansion is disabled, just copy */ + if (zstr_disable_token_expansion) { + zstr_cat(&result.expanded, text); + return result; + } + + /* Pre-reserve buffer space to avoid reallocations. + * Estimate: input length + overhead for ANSI codes (~50% extra) */ + size_t input_len = strlen(text); + zstr_reserve(&result.expanded, input_len + input_len / 2 + 64); + + /* Initialize parser state */ + TokenParser parser = {0}; + parser.out = &result.expanded; + parser.cursor_col = -1; + parser.cursor_row = -1; + parser.visual_col = 1; + parser.visual_row = 1; + parser.no_colors = zstr_no_colors; + + /* Ragel variables */ + int cs; + int act; + const char *p = text; + const char *pe = text + strlen(text); + const char *eof = pe; + const char *ts = NULL; /* Token start */ + const char *te = NULL; /* Token end */ + const char *tok_start = NULL; + const char *tok_end = NULL; + const char *goto_row_start = NULL; + const char *goto_row_end = NULL; + const char *goto_col_start = NULL; + const char *goto_col_end = NULL; + + (void)tok_start; + (void)tok_end; + (void)goto_row_start; + (void)goto_row_end; + (void)goto_col_start; + (void)goto_col_end; + (void)eof; + (void)ts; + (void)te; + (void)act; + (void)token_parser_en_main; + + +#line 629 "src/tokens.c" + { + cs = token_parser_start; + ts = 0; + te = 0; + act = 0; + } + +#line 954 "src/tokens.rl" + +#line 639 "src/tokens.c" + { + if ( p == pe ) + goto _test_eof; + switch ( cs ) + { +tr2: +#line 858 "src/tokens.rl" + {te = p+1;{ + for (const char *c = ts; c < te; c++) { + zstr_push(parser.out, *c); + } + }} + goto st242; +tr3: +#line 845 "src/tokens.rl" + {{p = ((te))-1;}{ + sync_styles(&parser); /* Emit any pending style changes */ + zstr_push(parser.out, *ts); + parser.visual_col++; + }} + goto st242; +tr291: +#line 845 "src/tokens.rl" + {te = p+1;{ + sync_styles(&parser); /* Emit any pending style changes */ + zstr_push(parser.out, *ts); + parser.visual_col++; + }} + goto st242; +tr292: +#line 851 "src/tokens.rl" + {te = p+1;{ + /* Auto-reset all active styles before newline */ + reset_line_styles(&parser); + zstr_push(parser.out, *ts); + parser.visual_col = 1; + }} + goto st242; +tr295: +#line 845 "src/tokens.rl" + {te = p;p--;{ + sync_styles(&parser); /* Emit any pending style changes */ + zstr_push(parser.out, *ts); + parser.visual_col++; + }} + goto st242; +tr311: +#line 584 "src/tokens.rl" + { pop_style(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr312: +#line 584 "src/tokens.rl" + { pop_style(&parser); } +#line 591 "src/tokens.rl" + { + push_attr(&parser, ATTR_BG, parser.bg_color); + parser.bg_color = 0; + mark_dirty(&parser); + } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr313: +#line 584 "src/tokens.rl" + { pop_style(&parser); } +#line 586 "src/tokens.rl" + { + push_attr(&parser, ATTR_FG, parser.fg_color); + parser.fg_color = 0; + mark_dirty(&parser); + } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr314: +#line 613 "src/tokens.rl" + { apply_bold(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr315: +#line 614 "src/tokens.rl" + { apply_italic(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr316: +#line 615 "src/tokens.rl" + { apply_underline(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr317: +#line 660 "src/tokens.rl" + { + int n = parse_number(tok_start, tok_end); + if (n >= 0 && n <= 255) { + apply_bg_256(&parser, n); + } + } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr318: +#line 642 "src/tokens.rl" + { apply_bg_code(&parser, 40); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr319: +#line 646 "src/tokens.rl" + { apply_bg_code(&parser, 44); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr320: +#line 648 "src/tokens.rl" + { apply_bg_code(&parser, 46); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr321: +#line 650 "src/tokens.rl" + { apply_bg_code(&parser, 100); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr322: +#line 644 "src/tokens.rl" + { apply_bg_code(&parser, 42); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr323: +#line 647 "src/tokens.rl" + { apply_bg_code(&parser, 45); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr324: +#line 643 "src/tokens.rl" + { apply_bg_code(&parser, 41); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr325: +#line 649 "src/tokens.rl" + { apply_bg_code(&parser, 47); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr326: +#line 645 "src/tokens.rl" + { apply_bg_code(&parser, 43); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr327: +#line 621 "src/tokens.rl" + { apply_fg_code(&parser, 30); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr328: +#line 625 "src/tokens.rl" + { apply_fg_code(&parser, 34); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr329: +#line 632 "src/tokens.rl" + { apply_fg_code(&parser, 90); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr330: +#line 636 "src/tokens.rl" + { apply_fg_code(&parser, 94); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr331: +#line 638 "src/tokens.rl" + { apply_fg_code(&parser, 96); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr332: +#line 634 "src/tokens.rl" + { apply_fg_code(&parser, 92); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr333: +#line 637 "src/tokens.rl" + { apply_fg_code(&parser, 95); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr334: +#line 633 "src/tokens.rl" + { apply_fg_code(&parser, 91); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr335: +#line 639 "src/tokens.rl" + { apply_fg_code(&parser, 97); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr336: +#line 635 "src/tokens.rl" + { apply_fg_code(&parser, 93); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr337: +#line 618 "src/tokens.rl" + { apply_bright(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr338: +#line 598 "src/tokens.rl" + { apply_token_b(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr339: +#line 739 "src/tokens.rl" + { emit_ansi(&parser, "\033[K"); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr340: +#line 740 "src/tokens.rl" + { emit_ansi(&parser, "\033[J"); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr341: +#line 571 "src/tokens.rl" + { + parser.cursor_col = parser.visual_col; + parser.cursor_row = parser.visual_row; + parser.has_cursor = true; + } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr342: +#line 627 "src/tokens.rl" + { apply_fg_code(&parser, 36); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr343: +#line 609 "src/tokens.rl" + { apply_token_danger(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr344: +#line 607 "src/tokens.rl" + { apply_token_dim_style(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr345: +#line 653 "src/tokens.rl" + { + int n = parse_number(tok_start, tok_end); + if (n >= 0 && n <= 255) { + apply_fg_256(&parser, n); + } + } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr346: +#line 750 "src/tokens.rl" + { + int row = parse_number(goto_row_start, goto_row_end); + int col = parse_number(goto_col_start, goto_col_end); + if (row >= 0 && col >= 0) { + emit_ansi_num(&parser, "\033[", row, ";"); + emit_ansi_num(&parser, "", col, "H"); + } + } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr347: +#line 577 "src/tokens.rl" + { + if (parser.cursor_row > 0 && parser.cursor_col > 0) { + emit_ansi_num(&parser, "\033[", parser.cursor_row, ";"); + emit_ansi_num(&parser, "", parser.cursor_col, "H"); + } + } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr348: +#line 629 "src/tokens.rl" + { apply_fg_code(&parser, 90); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr349: +#line 623 "src/tokens.rl" + { apply_fg_code(&parser, 32); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr350: +#line 600 "src/tokens.rl" + { apply_token_h1(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr351: +#line 601 "src/tokens.rl" + { apply_token_h2(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr352: +#line 602 "src/tokens.rl" + { apply_token_h3(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr353: +#line 603 "src/tokens.rl" + { apply_token_h4(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr354: +#line 604 "src/tokens.rl" + { apply_token_h5(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr355: +#line 605 "src/tokens.rl" + { apply_token_h6(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr356: +#line 742 "src/tokens.rl" + { emit_ansi(&parser, "\033[?25l"); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr357: +#line 599 "src/tokens.rl" + { apply_token_highlight(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr358: +#line 741 "src/tokens.rl" + { emit_ansi(&parser, "\033[H"); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr359: +#line 626 "src/tokens.rl" + { apply_fg_code(&parser, 35); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr360: +#line 622 "src/tokens.rl" + { apply_fg_code(&parser, 31); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr361: +#line 585 "src/tokens.rl" + { reset_all(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr362: +#line 616 "src/tokens.rl" + { apply_reverse(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr363: +#line 608 "src/tokens.rl" + { apply_token_section(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr364: +#line 743 "src/tokens.rl" + { emit_ansi(&parser, "\033[?25h"); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr365: +#line 617 "src/tokens.rl" + { apply_strikethrough(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr366: +#line 606 "src/tokens.rl" + { apply_token_strong(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr367: +#line 610 "src/tokens.rl" + { apply_token_text(&parser); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr368: +#line 628 "src/tokens.rl" + { apply_fg_code(&parser, 37); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +tr369: +#line 624 "src/tokens.rl" + { apply_fg_code(&parser, 33); } +#line 879 "src/tokens.rl" + {te = p;p--;} + goto st242; +st242: +#line 1 "NONE" + {ts = 0;} + if ( ++p == pe ) + goto _test_eof242; +case 242: +#line 1 "NONE" + {ts = p;} +#line 1086 "src/tokens.c" + switch( (*p) ) { + case 10: goto tr292; + case 27: goto st1; + case 123: goto tr294; + } + goto tr291; +st1: + if ( ++p == pe ) + goto _test_eof1; +case 1: + if ( (*p) == 91 ) + goto st2; + goto st0; +st0: +cs = 0; + goto _out; +st2: + if ( ++p == pe ) + goto _test_eof2; +case 2: + if ( (*p) > 90 ) { + if ( 97 <= (*p) && (*p) <= 122 ) + goto tr2; + } else if ( (*p) >= 65 ) + goto tr2; + goto st2; +tr294: +#line 1 "NONE" + {te = p+1;} + goto st243; +st243: + if ( ++p == pe ) + goto _test_eof243; +case 243: +#line 1121 "src/tokens.c" + switch( (*p) ) { + case 47: goto st3; + case 66: goto st9; + case 73: goto st10; + case 85: goto st11; + case 98: goto st12; + case 99: goto st106; + case 100: goto st118; + case 102: goto st126; + case 103: goto st130; + case 104: goto st151; + case 105: goto st171; + case 109: goto st176; + case 114: goto st183; + case 115: goto st194; + case 116: goto st219; + case 117: goto st223; + case 119: goto st231; + case 121: goto st236; + } + goto tr295; +st3: + if ( ++p == pe ) + goto _test_eof3; +case 3: + switch( (*p) ) { + case 98: goto st5; + case 102: goto st7; + case 125: goto st244; + } + if ( 97 <= (*p) && (*p) <= 122 ) + goto st4; + goto tr3; +st4: + if ( ++p == pe ) + goto _test_eof4; +case 4: + if ( (*p) == 125 ) + goto st244; + if ( 97 <= (*p) && (*p) <= 122 ) + goto st4; + goto tr3; +st244: + if ( ++p == pe ) + goto _test_eof244; +case 244: + goto tr311; +st5: + if ( ++p == pe ) + goto _test_eof5; +case 5: + switch( (*p) ) { + case 103: goto st6; + case 125: goto st244; + } + if ( 97 <= (*p) && (*p) <= 122 ) + goto st4; + goto tr3; +st6: + if ( ++p == pe ) + goto _test_eof6; +case 6: + if ( (*p) == 125 ) + goto st245; + if ( 97 <= (*p) && (*p) <= 122 ) + goto st4; + goto tr3; +st245: + if ( ++p == pe ) + goto _test_eof245; +case 245: + goto tr312; +st7: + if ( ++p == pe ) + goto _test_eof7; +case 7: + switch( (*p) ) { + case 103: goto st8; + case 125: goto st244; + } + if ( 97 <= (*p) && (*p) <= 122 ) + goto st4; + goto tr3; +st8: + if ( ++p == pe ) + goto _test_eof8; +case 8: + if ( (*p) == 125 ) + goto st246; + if ( 97 <= (*p) && (*p) <= 122 ) + goto st4; + goto tr3; +st246: + if ( ++p == pe ) + goto _test_eof246; +case 246: + goto tr313; +st9: + if ( ++p == pe ) + goto _test_eof9; +case 9: + if ( (*p) == 125 ) + goto st247; + goto tr3; +st247: + if ( ++p == pe ) + goto _test_eof247; +case 247: + goto tr314; +st10: + if ( ++p == pe ) + goto _test_eof10; +case 10: + if ( (*p) == 125 ) + goto st248; + goto tr3; +st248: + if ( ++p == pe ) + goto _test_eof248; +case 248: + goto tr315; +st11: + if ( ++p == pe ) + goto _test_eof11; +case 11: + if ( (*p) == 125 ) + goto st249; + goto tr3; +st249: + if ( ++p == pe ) + goto _test_eof249; +case 249: + goto tr316; +st12: + if ( ++p == pe ) + goto _test_eof12; +case 12: + switch( (*p) ) { + case 103: goto st13; + case 108: goto st55; + case 111: goto st61; + case 114: goto st63; + case 125: goto st271; + } + goto tr3; +st13: + if ( ++p == pe ) + goto _test_eof13; +case 13: + if ( (*p) == 58 ) + goto st14; + goto tr3; +st14: + if ( ++p == pe ) + goto _test_eof14; +case 14: + switch( (*p) ) { + case 98: goto st16; + case 99: goto st23; + case 103: goto st27; + case 109: goto st34; + case 114: goto st41; + case 119: goto st44; + case 121: goto st49; + } + if ( 48 <= (*p) && (*p) <= 57 ) + goto tr21; + goto tr3; +tr21: +#line 555 "src/tokens.rl" + { tok_start = p; } + goto st15; +st15: + if ( ++p == pe ) + goto _test_eof15; +case 15: +#line 1298 "src/tokens.c" + if ( (*p) == 125 ) + goto tr30; + if ( 48 <= (*p) && (*p) <= 57 ) + goto st15; + goto tr3; +tr30: +#line 556 "src/tokens.rl" + { tok_end = p; } + goto st250; +st250: + if ( ++p == pe ) + goto _test_eof250; +case 250: +#line 1312 "src/tokens.c" + goto tr317; +st16: + if ( ++p == pe ) + goto _test_eof16; +case 16: + if ( (*p) == 108 ) + goto st17; + goto tr3; +st17: + if ( ++p == pe ) + goto _test_eof17; +case 17: + switch( (*p) ) { + case 97: goto st18; + case 117: goto st21; + } + goto tr3; +st18: + if ( ++p == pe ) + goto _test_eof18; +case 18: + if ( (*p) == 99 ) + goto st19; + goto tr3; +st19: + if ( ++p == pe ) + goto _test_eof19; +case 19: + if ( (*p) == 107 ) + goto st20; + goto tr3; +st20: + if ( ++p == pe ) + goto _test_eof20; +case 20: + if ( (*p) == 125 ) + goto st251; + goto tr3; +st251: + if ( ++p == pe ) + goto _test_eof251; +case 251: + goto tr318; +st21: + if ( ++p == pe ) + goto _test_eof21; +case 21: + if ( (*p) == 101 ) + goto st22; + goto tr3; +st22: + if ( ++p == pe ) + goto _test_eof22; +case 22: + if ( (*p) == 125 ) + goto st252; + goto tr3; +st252: + if ( ++p == pe ) + goto _test_eof252; +case 252: + goto tr319; +st23: + if ( ++p == pe ) + goto _test_eof23; +case 23: + if ( (*p) == 121 ) + goto st24; + goto tr3; +st24: + if ( ++p == pe ) + goto _test_eof24; +case 24: + if ( (*p) == 97 ) + goto st25; + goto tr3; +st25: + if ( ++p == pe ) + goto _test_eof25; +case 25: + if ( (*p) == 110 ) + goto st26; + goto tr3; +st26: + if ( ++p == pe ) + goto _test_eof26; +case 26: + if ( (*p) == 125 ) + goto st253; + goto tr3; +st253: + if ( ++p == pe ) + goto _test_eof253; +case 253: + goto tr320; +st27: + if ( ++p == pe ) + goto _test_eof27; +case 27: + if ( (*p) == 114 ) + goto st28; + goto tr3; +st28: + if ( ++p == pe ) + goto _test_eof28; +case 28: + switch( (*p) ) { + case 97: goto st29; + case 101: goto st31; + } + goto tr3; +st29: + if ( ++p == pe ) + goto _test_eof29; +case 29: + if ( (*p) == 121 ) + goto st30; + goto tr3; +st30: + if ( ++p == pe ) + goto _test_eof30; +case 30: + if ( (*p) == 125 ) + goto st254; + goto tr3; +st254: + if ( ++p == pe ) + goto _test_eof254; +case 254: + goto tr321; +st31: + if ( ++p == pe ) + goto _test_eof31; +case 31: + switch( (*p) ) { + case 101: goto st32; + case 121: goto st30; + } + goto tr3; +st32: + if ( ++p == pe ) + goto _test_eof32; +case 32: + if ( (*p) == 110 ) + goto st33; + goto tr3; +st33: + if ( ++p == pe ) + goto _test_eof33; +case 33: + if ( (*p) == 125 ) + goto st255; + goto tr3; +st255: + if ( ++p == pe ) + goto _test_eof255; +case 255: + goto tr322; +st34: + if ( ++p == pe ) + goto _test_eof34; +case 34: + if ( (*p) == 97 ) + goto st35; + goto tr3; +st35: + if ( ++p == pe ) + goto _test_eof35; +case 35: + if ( (*p) == 103 ) + goto st36; + goto tr3; +st36: + if ( ++p == pe ) + goto _test_eof36; +case 36: + if ( (*p) == 101 ) + goto st37; + goto tr3; +st37: + if ( ++p == pe ) + goto _test_eof37; +case 37: + if ( (*p) == 110 ) + goto st38; + goto tr3; +st38: + if ( ++p == pe ) + goto _test_eof38; +case 38: + if ( (*p) == 116 ) + goto st39; + goto tr3; +st39: + if ( ++p == pe ) + goto _test_eof39; +case 39: + if ( (*p) == 97 ) + goto st40; + goto tr3; +st40: + if ( ++p == pe ) + goto _test_eof40; +case 40: + if ( (*p) == 125 ) + goto st256; + goto tr3; +st256: + if ( ++p == pe ) + goto _test_eof256; +case 256: + goto tr323; +st41: + if ( ++p == pe ) + goto _test_eof41; +case 41: + if ( (*p) == 101 ) + goto st42; + goto tr3; +st42: + if ( ++p == pe ) + goto _test_eof42; +case 42: + if ( (*p) == 100 ) + goto st43; + goto tr3; +st43: + if ( ++p == pe ) + goto _test_eof43; +case 43: + if ( (*p) == 125 ) + goto st257; + goto tr3; +st257: + if ( ++p == pe ) + goto _test_eof257; +case 257: + goto tr324; +st44: + if ( ++p == pe ) + goto _test_eof44; +case 44: + if ( (*p) == 104 ) + goto st45; + goto tr3; +st45: + if ( ++p == pe ) + goto _test_eof45; +case 45: + if ( (*p) == 105 ) + goto st46; + goto tr3; +st46: + if ( ++p == pe ) + goto _test_eof46; +case 46: + if ( (*p) == 116 ) + goto st47; + goto tr3; +st47: + if ( ++p == pe ) + goto _test_eof47; +case 47: + if ( (*p) == 101 ) + goto st48; + goto tr3; +st48: + if ( ++p == pe ) + goto _test_eof48; +case 48: + if ( (*p) == 125 ) + goto st258; + goto tr3; +st258: + if ( ++p == pe ) + goto _test_eof258; +case 258: + goto tr325; +st49: + if ( ++p == pe ) + goto _test_eof49; +case 49: + if ( (*p) == 101 ) + goto st50; + goto tr3; +st50: + if ( ++p == pe ) + goto _test_eof50; +case 50: + if ( (*p) == 108 ) + goto st51; + goto tr3; +st51: + if ( ++p == pe ) + goto _test_eof51; +case 51: + if ( (*p) == 108 ) + goto st52; + goto tr3; +st52: + if ( ++p == pe ) + goto _test_eof52; +case 52: + if ( (*p) == 111 ) + goto st53; + goto tr3; +st53: + if ( ++p == pe ) + goto _test_eof53; +case 53: + if ( (*p) == 119 ) + goto st54; + goto tr3; +st54: + if ( ++p == pe ) + goto _test_eof54; +case 54: + if ( (*p) == 125 ) + goto st259; + goto tr3; +st259: + if ( ++p == pe ) + goto _test_eof259; +case 259: + goto tr326; +st55: + if ( ++p == pe ) + goto _test_eof55; +case 55: + switch( (*p) ) { + case 97: goto st56; + case 117: goto st59; + } + goto tr3; +st56: + if ( ++p == pe ) + goto _test_eof56; +case 56: + if ( (*p) == 99 ) + goto st57; + goto tr3; +st57: + if ( ++p == pe ) + goto _test_eof57; +case 57: + if ( (*p) == 107 ) + goto st58; + goto tr3; +st58: + if ( ++p == pe ) + goto _test_eof58; +case 58: + if ( (*p) == 125 ) + goto st260; + goto tr3; +st260: + if ( ++p == pe ) + goto _test_eof260; +case 260: + goto tr327; +st59: + if ( ++p == pe ) + goto _test_eof59; +case 59: + if ( (*p) == 101 ) + goto st60; + goto tr3; +st60: + if ( ++p == pe ) + goto _test_eof60; +case 60: + if ( (*p) == 125 ) + goto st261; + goto tr3; +st261: + if ( ++p == pe ) + goto _test_eof261; +case 261: + goto tr328; +st61: + if ( ++p == pe ) + goto _test_eof61; +case 61: + if ( (*p) == 108 ) + goto st62; + goto tr3; +st62: + if ( ++p == pe ) + goto _test_eof62; +case 62: + if ( (*p) == 100 ) + goto st9; + goto tr3; +st63: + if ( ++p == pe ) + goto _test_eof63; +case 63: + if ( (*p) == 105 ) + goto st64; + goto tr3; +st64: + if ( ++p == pe ) + goto _test_eof64; +case 64: + if ( (*p) == 103 ) + goto st65; + goto tr3; +st65: + if ( ++p == pe ) + goto _test_eof65; +case 65: + if ( (*p) == 104 ) + goto st66; + goto tr3; +st66: + if ( ++p == pe ) + goto _test_eof66; +case 66: + if ( (*p) == 116 ) + goto st67; + goto tr3; +st67: + if ( ++p == pe ) + goto _test_eof67; +case 67: + switch( (*p) ) { + case 58: goto st68; + case 125: goto st270; + } + goto tr3; +st68: + if ( ++p == pe ) + goto _test_eof68; +case 68: + switch( (*p) ) { + case 98: goto st69; + case 99: goto st76; + case 103: goto st80; + case 109: goto st85; + case 114: goto st92; + case 119: goto st95; + case 121: goto st100; + } + goto tr3; +st69: + if ( ++p == pe ) + goto _test_eof69; +case 69: + if ( (*p) == 108 ) + goto st70; + goto tr3; +st70: + if ( ++p == pe ) + goto _test_eof70; +case 70: + switch( (*p) ) { + case 97: goto st71; + case 117: goto st74; + } + goto tr3; +st71: + if ( ++p == pe ) + goto _test_eof71; +case 71: + if ( (*p) == 99 ) + goto st72; + goto tr3; +st72: + if ( ++p == pe ) + goto _test_eof72; +case 72: + if ( (*p) == 107 ) + goto st73; + goto tr3; +st73: + if ( ++p == pe ) + goto _test_eof73; +case 73: + if ( (*p) == 125 ) + goto st262; + goto tr3; +st262: + if ( ++p == pe ) + goto _test_eof262; +case 262: + goto tr329; +st74: + if ( ++p == pe ) + goto _test_eof74; +case 74: + if ( (*p) == 101 ) + goto st75; + goto tr3; +st75: + if ( ++p == pe ) + goto _test_eof75; +case 75: + if ( (*p) == 125 ) + goto st263; + goto tr3; +st263: + if ( ++p == pe ) + goto _test_eof263; +case 263: + goto tr330; +st76: + if ( ++p == pe ) + goto _test_eof76; +case 76: + if ( (*p) == 121 ) + goto st77; + goto tr3; +st77: + if ( ++p == pe ) + goto _test_eof77; +case 77: + if ( (*p) == 97 ) + goto st78; + goto tr3; +st78: + if ( ++p == pe ) + goto _test_eof78; +case 78: + if ( (*p) == 110 ) + goto st79; + goto tr3; +st79: + if ( ++p == pe ) + goto _test_eof79; +case 79: + if ( (*p) == 125 ) + goto st264; + goto tr3; +st264: + if ( ++p == pe ) + goto _test_eof264; +case 264: + goto tr331; +st80: + if ( ++p == pe ) + goto _test_eof80; +case 80: + if ( (*p) == 114 ) + goto st81; + goto tr3; +st81: + if ( ++p == pe ) + goto _test_eof81; +case 81: + if ( (*p) == 101 ) + goto st82; + goto tr3; +st82: + if ( ++p == pe ) + goto _test_eof82; +case 82: + if ( (*p) == 101 ) + goto st83; + goto tr3; +st83: + if ( ++p == pe ) + goto _test_eof83; +case 83: + if ( (*p) == 110 ) + goto st84; + goto tr3; +st84: + if ( ++p == pe ) + goto _test_eof84; +case 84: + if ( (*p) == 125 ) + goto st265; + goto tr3; +st265: + if ( ++p == pe ) + goto _test_eof265; +case 265: + goto tr332; +st85: + if ( ++p == pe ) + goto _test_eof85; +case 85: + if ( (*p) == 97 ) + goto st86; + goto tr3; +st86: + if ( ++p == pe ) + goto _test_eof86; +case 86: + if ( (*p) == 103 ) + goto st87; + goto tr3; +st87: + if ( ++p == pe ) + goto _test_eof87; +case 87: + if ( (*p) == 101 ) + goto st88; + goto tr3; +st88: + if ( ++p == pe ) + goto _test_eof88; +case 88: + if ( (*p) == 110 ) + goto st89; + goto tr3; +st89: + if ( ++p == pe ) + goto _test_eof89; +case 89: + if ( (*p) == 116 ) + goto st90; + goto tr3; +st90: + if ( ++p == pe ) + goto _test_eof90; +case 90: + if ( (*p) == 97 ) + goto st91; + goto tr3; +st91: + if ( ++p == pe ) + goto _test_eof91; +case 91: + if ( (*p) == 125 ) + goto st266; + goto tr3; +st266: + if ( ++p == pe ) + goto _test_eof266; +case 266: + goto tr333; +st92: + if ( ++p == pe ) + goto _test_eof92; +case 92: + if ( (*p) == 101 ) + goto st93; + goto tr3; +st93: + if ( ++p == pe ) + goto _test_eof93; +case 93: + if ( (*p) == 100 ) + goto st94; + goto tr3; +st94: + if ( ++p == pe ) + goto _test_eof94; +case 94: + if ( (*p) == 125 ) + goto st267; + goto tr3; +st267: + if ( ++p == pe ) + goto _test_eof267; +case 267: + goto tr334; +st95: + if ( ++p == pe ) + goto _test_eof95; +case 95: + if ( (*p) == 104 ) + goto st96; + goto tr3; +st96: + if ( ++p == pe ) + goto _test_eof96; +case 96: + if ( (*p) == 105 ) + goto st97; + goto tr3; +st97: + if ( ++p == pe ) + goto _test_eof97; +case 97: + if ( (*p) == 116 ) + goto st98; + goto tr3; +st98: + if ( ++p == pe ) + goto _test_eof98; +case 98: + if ( (*p) == 101 ) + goto st99; + goto tr3; +st99: + if ( ++p == pe ) + goto _test_eof99; +case 99: + if ( (*p) == 125 ) + goto st268; + goto tr3; +st268: + if ( ++p == pe ) + goto _test_eof268; +case 268: + goto tr335; +st100: + if ( ++p == pe ) + goto _test_eof100; +case 100: + if ( (*p) == 101 ) + goto st101; + goto tr3; +st101: + if ( ++p == pe ) + goto _test_eof101; +case 101: + if ( (*p) == 108 ) + goto st102; + goto tr3; +st102: + if ( ++p == pe ) + goto _test_eof102; +case 102: + if ( (*p) == 108 ) + goto st103; + goto tr3; +st103: + if ( ++p == pe ) + goto _test_eof103; +case 103: + if ( (*p) == 111 ) + goto st104; + goto tr3; +st104: + if ( ++p == pe ) + goto _test_eof104; +case 104: + if ( (*p) == 119 ) + goto st105; + goto tr3; +st105: + if ( ++p == pe ) + goto _test_eof105; +case 105: + if ( (*p) == 125 ) + goto st269; + goto tr3; +st269: + if ( ++p == pe ) + goto _test_eof269; +case 269: + goto tr336; +st270: + if ( ++p == pe ) + goto _test_eof270; +case 270: + goto tr337; +st271: + if ( ++p == pe ) + goto _test_eof271; +case 271: + goto tr338; +st106: + if ( ++p == pe ) + goto _test_eof106; +case 106: + switch( (*p) ) { + case 108: goto st107; + case 117: goto st110; + case 121: goto st115; + } + goto tr3; +st107: + if ( ++p == pe ) + goto _test_eof107; +case 107: + switch( (*p) ) { + case 114: goto st108; + case 115: goto st109; + } + goto tr3; +st108: + if ( ++p == pe ) + goto _test_eof108; +case 108: + if ( (*p) == 125 ) + goto st272; + goto tr3; +st272: + if ( ++p == pe ) + goto _test_eof272; +case 272: + goto tr339; +st109: + if ( ++p == pe ) + goto _test_eof109; +case 109: + if ( (*p) == 125 ) + goto st273; + goto tr3; +st273: + if ( ++p == pe ) + goto _test_eof273; +case 273: + goto tr340; +st110: + if ( ++p == pe ) + goto _test_eof110; +case 110: + if ( (*p) == 114 ) + goto st111; + goto tr3; +st111: + if ( ++p == pe ) + goto _test_eof111; +case 111: + if ( (*p) == 115 ) + goto st112; + goto tr3; +st112: + if ( ++p == pe ) + goto _test_eof112; +case 112: + if ( (*p) == 111 ) + goto st113; + goto tr3; +st113: + if ( ++p == pe ) + goto _test_eof113; +case 113: + if ( (*p) == 114 ) + goto st114; + goto tr3; +st114: + if ( ++p == pe ) + goto _test_eof114; +case 114: + if ( (*p) == 125 ) + goto st274; + goto tr3; +st274: + if ( ++p == pe ) + goto _test_eof274; +case 274: + goto tr341; +st115: + if ( ++p == pe ) + goto _test_eof115; +case 115: + if ( (*p) == 97 ) + goto st116; + goto tr3; +st116: + if ( ++p == pe ) + goto _test_eof116; +case 116: + if ( (*p) == 110 ) + goto st117; + goto tr3; +st117: + if ( ++p == pe ) + goto _test_eof117; +case 117: + if ( (*p) == 125 ) + goto st275; + goto tr3; +st275: + if ( ++p == pe ) + goto _test_eof275; +case 275: + goto tr342; +st118: + if ( ++p == pe ) + goto _test_eof118; +case 118: + switch( (*p) ) { + case 97: goto st119; + case 105: goto st124; + } + goto tr3; +st119: + if ( ++p == pe ) + goto _test_eof119; +case 119: + if ( (*p) == 110 ) + goto st120; + goto tr3; +st120: + if ( ++p == pe ) + goto _test_eof120; +case 120: + if ( (*p) == 103 ) + goto st121; + goto tr3; +st121: + if ( ++p == pe ) + goto _test_eof121; +case 121: + if ( (*p) == 101 ) + goto st122; + goto tr3; +st122: + if ( ++p == pe ) + goto _test_eof122; +case 122: + if ( (*p) == 114 ) + goto st123; + goto tr3; +st123: + if ( ++p == pe ) + goto _test_eof123; +case 123: + if ( (*p) == 125 ) + goto st276; + goto tr3; +st276: + if ( ++p == pe ) + goto _test_eof276; +case 276: + goto tr343; +st124: + if ( ++p == pe ) + goto _test_eof124; +case 124: + if ( (*p) == 109 ) + goto st125; + goto tr3; +st125: + if ( ++p == pe ) + goto _test_eof125; +case 125: + if ( (*p) == 125 ) + goto st277; + goto tr3; +st277: + if ( ++p == pe ) + goto _test_eof277; +case 277: + goto tr344; +st126: + if ( ++p == pe ) + goto _test_eof126; +case 126: + if ( (*p) == 103 ) + goto st127; + goto tr3; +st127: + if ( ++p == pe ) + goto _test_eof127; +case 127: + if ( (*p) == 58 ) + goto st128; + goto tr3; +st128: + if ( ++p == pe ) + goto _test_eof128; +case 128: + if ( 48 <= (*p) && (*p) <= 57 ) + goto tr158; + goto tr3; +tr158: +#line 555 "src/tokens.rl" + { tok_start = p; } + goto st129; +st129: + if ( ++p == pe ) + goto _test_eof129; +case 129: +#line 2274 "src/tokens.c" + if ( (*p) == 125 ) + goto tr160; + if ( 48 <= (*p) && (*p) <= 57 ) + goto st129; + goto tr3; +tr160: +#line 556 "src/tokens.rl" + { tok_end = p; } + goto st278; +st278: + if ( ++p == pe ) + goto _test_eof278; +case 278: +#line 2288 "src/tokens.c" + goto tr345; +st130: + if ( ++p == pe ) + goto _test_eof130; +case 130: + switch( (*p) ) { + case 111: goto st131; + case 114: goto st145; + } + goto tr3; +st131: + if ( ++p == pe ) + goto _test_eof131; +case 131: + if ( (*p) == 116 ) + goto st132; + goto tr3; +st132: + if ( ++p == pe ) + goto _test_eof132; +case 132: + if ( (*p) == 111 ) + goto st133; + goto tr3; +st133: + if ( ++p == pe ) + goto _test_eof133; +case 133: + switch( (*p) ) { + case 58: goto st134; + case 95: goto st138; + } + goto tr3; +st134: + if ( ++p == pe ) + goto _test_eof134; +case 134: + if ( 48 <= (*p) && (*p) <= 57 ) + goto tr167; + goto tr3; +tr167: +#line 746 "src/tokens.rl" + { goto_row_start = p; } + goto st135; +st135: + if ( ++p == pe ) + goto _test_eof135; +case 135: +#line 2337 "src/tokens.c" + if ( (*p) == 44 ) + goto tr168; + if ( 48 <= (*p) && (*p) <= 57 ) + goto st135; + goto tr3; +tr168: +#line 747 "src/tokens.rl" + { goto_row_end = p; } + goto st136; +st136: + if ( ++p == pe ) + goto _test_eof136; +case 136: +#line 2351 "src/tokens.c" + if ( 48 <= (*p) && (*p) <= 57 ) + goto tr170; + goto tr3; +tr170: +#line 748 "src/tokens.rl" + { goto_col_start = p; } + goto st137; +st137: + if ( ++p == pe ) + goto _test_eof137; +case 137: +#line 2363 "src/tokens.c" + if ( (*p) == 125 ) + goto tr172; + if ( 48 <= (*p) && (*p) <= 57 ) + goto st137; + goto tr3; +tr172: +#line 749 "src/tokens.rl" + { goto_col_end = p; } + goto st279; +st279: + if ( ++p == pe ) + goto _test_eof279; +case 279: +#line 2377 "src/tokens.c" + goto tr346; +st138: + if ( ++p == pe ) + goto _test_eof138; +case 138: + if ( (*p) == 99 ) + goto st139; + goto tr3; +st139: + if ( ++p == pe ) + goto _test_eof139; +case 139: + if ( (*p) == 117 ) + goto st140; + goto tr3; +st140: + if ( ++p == pe ) + goto _test_eof140; +case 140: + if ( (*p) == 114 ) + goto st141; + goto tr3; +st141: + if ( ++p == pe ) + goto _test_eof141; +case 141: + if ( (*p) == 115 ) + goto st142; + goto tr3; +st142: + if ( ++p == pe ) + goto _test_eof142; +case 142: + if ( (*p) == 111 ) + goto st143; + goto tr3; +st143: + if ( ++p == pe ) + goto _test_eof143; +case 143: + if ( (*p) == 114 ) + goto st144; + goto tr3; +st144: + if ( ++p == pe ) + goto _test_eof144; +case 144: + if ( (*p) == 125 ) + goto st280; + goto tr3; +st280: + if ( ++p == pe ) + goto _test_eof280; +case 280: + goto tr347; +st145: + if ( ++p == pe ) + goto _test_eof145; +case 145: + switch( (*p) ) { + case 97: goto st146; + case 101: goto st148; + } + goto tr3; +st146: + if ( ++p == pe ) + goto _test_eof146; +case 146: + if ( (*p) == 121 ) + goto st147; + goto tr3; +st147: + if ( ++p == pe ) + goto _test_eof147; +case 147: + if ( (*p) == 125 ) + goto st281; + goto tr3; +st281: + if ( ++p == pe ) + goto _test_eof281; +case 281: + goto tr348; +st148: + if ( ++p == pe ) + goto _test_eof148; +case 148: + switch( (*p) ) { + case 101: goto st149; + case 121: goto st147; + } + goto tr3; +st149: + if ( ++p == pe ) + goto _test_eof149; +case 149: + if ( (*p) == 110 ) + goto st150; + goto tr3; +st150: + if ( ++p == pe ) + goto _test_eof150; +case 150: + if ( (*p) == 125 ) + goto st282; + goto tr3; +st282: + if ( ++p == pe ) + goto _test_eof282; +case 282: + goto tr349; +st151: + if ( ++p == pe ) + goto _test_eof151; +case 151: + switch( (*p) ) { + case 49: goto st152; + case 50: goto st153; + case 51: goto st154; + case 52: goto st155; + case 53: goto st156; + case 54: goto st157; + case 105: goto st158; + case 111: goto st168; + } + goto tr3; +st152: + if ( ++p == pe ) + goto _test_eof152; +case 152: + if ( (*p) == 125 ) + goto st283; + goto tr3; +st283: + if ( ++p == pe ) + goto _test_eof283; +case 283: + goto tr350; +st153: + if ( ++p == pe ) + goto _test_eof153; +case 153: + if ( (*p) == 125 ) + goto st284; + goto tr3; +st284: + if ( ++p == pe ) + goto _test_eof284; +case 284: + goto tr351; +st154: + if ( ++p == pe ) + goto _test_eof154; +case 154: + if ( (*p) == 125 ) + goto st285; + goto tr3; +st285: + if ( ++p == pe ) + goto _test_eof285; +case 285: + goto tr352; +st155: + if ( ++p == pe ) + goto _test_eof155; +case 155: + if ( (*p) == 125 ) + goto st286; + goto tr3; +st286: + if ( ++p == pe ) + goto _test_eof286; +case 286: + goto tr353; +st156: + if ( ++p == pe ) + goto _test_eof156; +case 156: + if ( (*p) == 125 ) + goto st287; + goto tr3; +st287: + if ( ++p == pe ) + goto _test_eof287; +case 287: + goto tr354; +st157: + if ( ++p == pe ) + goto _test_eof157; +case 157: + if ( (*p) == 125 ) + goto st288; + goto tr3; +st288: + if ( ++p == pe ) + goto _test_eof288; +case 288: + goto tr355; +st158: + if ( ++p == pe ) + goto _test_eof158; +case 158: + switch( (*p) ) { + case 100: goto st159; + case 103: goto st161; + } + goto tr3; +st159: + if ( ++p == pe ) + goto _test_eof159; +case 159: + if ( (*p) == 101 ) + goto st160; + goto tr3; +st160: + if ( ++p == pe ) + goto _test_eof160; +case 160: + if ( (*p) == 125 ) + goto st289; + goto tr3; +st289: + if ( ++p == pe ) + goto _test_eof289; +case 289: + goto tr356; +st161: + if ( ++p == pe ) + goto _test_eof161; +case 161: + if ( (*p) == 104 ) + goto st162; + goto tr3; +st162: + if ( ++p == pe ) + goto _test_eof162; +case 162: + if ( (*p) == 108 ) + goto st163; + goto tr3; +st163: + if ( ++p == pe ) + goto _test_eof163; +case 163: + if ( (*p) == 105 ) + goto st164; + goto tr3; +st164: + if ( ++p == pe ) + goto _test_eof164; +case 164: + if ( (*p) == 103 ) + goto st165; + goto tr3; +st165: + if ( ++p == pe ) + goto _test_eof165; +case 165: + if ( (*p) == 104 ) + goto st166; + goto tr3; +st166: + if ( ++p == pe ) + goto _test_eof166; +case 166: + if ( (*p) == 116 ) + goto st167; + goto tr3; +st167: + if ( ++p == pe ) + goto _test_eof167; +case 167: + if ( (*p) == 125 ) + goto st290; + goto tr3; +st290: + if ( ++p == pe ) + goto _test_eof290; +case 290: + goto tr357; +st168: + if ( ++p == pe ) + goto _test_eof168; +case 168: + if ( (*p) == 109 ) + goto st169; + goto tr3; +st169: + if ( ++p == pe ) + goto _test_eof169; +case 169: + if ( (*p) == 101 ) + goto st170; + goto tr3; +st170: + if ( ++p == pe ) + goto _test_eof170; +case 170: + if ( (*p) == 125 ) + goto st291; + goto tr3; +st291: + if ( ++p == pe ) + goto _test_eof291; +case 291: + goto tr358; +st171: + if ( ++p == pe ) + goto _test_eof171; +case 171: + switch( (*p) ) { + case 116: goto st172; + case 125: goto st248; + } + goto tr3; +st172: + if ( ++p == pe ) + goto _test_eof172; +case 172: + if ( (*p) == 97 ) + goto st173; + goto tr3; +st173: + if ( ++p == pe ) + goto _test_eof173; +case 173: + if ( (*p) == 108 ) + goto st174; + goto tr3; +st174: + if ( ++p == pe ) + goto _test_eof174; +case 174: + if ( (*p) == 105 ) + goto st175; + goto tr3; +st175: + if ( ++p == pe ) + goto _test_eof175; +case 175: + if ( (*p) == 99 ) + goto st10; + goto tr3; +st176: + if ( ++p == pe ) + goto _test_eof176; +case 176: + if ( (*p) == 97 ) + goto st177; + goto tr3; +st177: + if ( ++p == pe ) + goto _test_eof177; +case 177: + if ( (*p) == 103 ) + goto st178; + goto tr3; +st178: + if ( ++p == pe ) + goto _test_eof178; +case 178: + if ( (*p) == 101 ) + goto st179; + goto tr3; +st179: + if ( ++p == pe ) + goto _test_eof179; +case 179: + if ( (*p) == 110 ) + goto st180; + goto tr3; +st180: + if ( ++p == pe ) + goto _test_eof180; +case 180: + if ( (*p) == 116 ) + goto st181; + goto tr3; +st181: + if ( ++p == pe ) + goto _test_eof181; +case 181: + if ( (*p) == 97 ) + goto st182; + goto tr3; +st182: + if ( ++p == pe ) + goto _test_eof182; +case 182: + if ( (*p) == 125 ) + goto st292; + goto tr3; +st292: + if ( ++p == pe ) + goto _test_eof292; +case 292: + goto tr359; +st183: + if ( ++p == pe ) + goto _test_eof183; +case 183: + if ( (*p) == 101 ) + goto st184; + goto tr3; +st184: + if ( ++p == pe ) + goto _test_eof184; +case 184: + switch( (*p) ) { + case 100: goto st185; + case 115: goto st186; + case 118: goto st189; + } + goto tr3; +st185: + if ( ++p == pe ) + goto _test_eof185; +case 185: + if ( (*p) == 125 ) + goto st293; + goto tr3; +st293: + if ( ++p == pe ) + goto _test_eof293; +case 293: + goto tr360; +st186: + if ( ++p == pe ) + goto _test_eof186; +case 186: + if ( (*p) == 101 ) + goto st187; + goto tr3; +st187: + if ( ++p == pe ) + goto _test_eof187; +case 187: + if ( (*p) == 116 ) + goto st188; + goto tr3; +st188: + if ( ++p == pe ) + goto _test_eof188; +case 188: + if ( (*p) == 125 ) + goto st294; + goto tr3; +st294: + if ( ++p == pe ) + goto _test_eof294; +case 294: + goto tr361; +st189: + if ( ++p == pe ) + goto _test_eof189; +case 189: + if ( (*p) == 101 ) + goto st190; + goto tr3; +st190: + if ( ++p == pe ) + goto _test_eof190; +case 190: + if ( (*p) == 114 ) + goto st191; + goto tr3; +st191: + if ( ++p == pe ) + goto _test_eof191; +case 191: + if ( (*p) == 115 ) + goto st192; + goto tr3; +st192: + if ( ++p == pe ) + goto _test_eof192; +case 192: + if ( (*p) == 101 ) + goto st193; + goto tr3; +st193: + if ( ++p == pe ) + goto _test_eof193; +case 193: + if ( (*p) == 125 ) + goto st295; + goto tr3; +st295: + if ( ++p == pe ) + goto _test_eof295; +case 295: + goto tr362; +st194: + if ( ++p == pe ) + goto _test_eof194; +case 194: + switch( (*p) ) { + case 101: goto st195; + case 104: goto st201; + case 116: goto st204; + } + goto tr3; +st195: + if ( ++p == pe ) + goto _test_eof195; +case 195: + if ( (*p) == 99 ) + goto st196; + goto tr3; +st196: + if ( ++p == pe ) + goto _test_eof196; +case 196: + if ( (*p) == 116 ) + goto st197; + goto tr3; +st197: + if ( ++p == pe ) + goto _test_eof197; +case 197: + if ( (*p) == 105 ) + goto st198; + goto tr3; +st198: + if ( ++p == pe ) + goto _test_eof198; +case 198: + if ( (*p) == 111 ) + goto st199; + goto tr3; +st199: + if ( ++p == pe ) + goto _test_eof199; +case 199: + if ( (*p) == 110 ) + goto st200; + goto tr3; +st200: + if ( ++p == pe ) + goto _test_eof200; +case 200: + if ( (*p) == 125 ) + goto st296; + goto tr3; +st296: + if ( ++p == pe ) + goto _test_eof296; +case 296: + goto tr363; +st201: + if ( ++p == pe ) + goto _test_eof201; +case 201: + if ( (*p) == 111 ) + goto st202; + goto tr3; +st202: + if ( ++p == pe ) + goto _test_eof202; +case 202: + if ( (*p) == 119 ) + goto st203; + goto tr3; +st203: + if ( ++p == pe ) + goto _test_eof203; +case 203: + if ( (*p) == 125 ) + goto st297; + goto tr3; +st297: + if ( ++p == pe ) + goto _test_eof297; +case 297: + goto tr364; +st204: + if ( ++p == pe ) + goto _test_eof204; +case 204: + if ( (*p) == 114 ) + goto st205; + goto tr3; +st205: + if ( ++p == pe ) + goto _test_eof205; +case 205: + switch( (*p) ) { + case 105: goto st206; + case 111: goto st216; + } + goto tr3; +st206: + if ( ++p == pe ) + goto _test_eof206; +case 206: + if ( (*p) == 107 ) + goto st207; + goto tr3; +st207: + if ( ++p == pe ) + goto _test_eof207; +case 207: + if ( (*p) == 101 ) + goto st208; + goto tr3; +st208: + if ( ++p == pe ) + goto _test_eof208; +case 208: + switch( (*p) ) { + case 116: goto st209; + case 125: goto st298; + } + goto tr3; +st209: + if ( ++p == pe ) + goto _test_eof209; +case 209: + if ( (*p) == 104 ) + goto st210; + goto tr3; +st210: + if ( ++p == pe ) + goto _test_eof210; +case 210: + if ( (*p) == 114 ) + goto st211; + goto tr3; +st211: + if ( ++p == pe ) + goto _test_eof211; +case 211: + if ( (*p) == 111 ) + goto st212; + goto tr3; +st212: + if ( ++p == pe ) + goto _test_eof212; +case 212: + if ( (*p) == 117 ) + goto st213; + goto tr3; +st213: + if ( ++p == pe ) + goto _test_eof213; +case 213: + if ( (*p) == 103 ) + goto st214; + goto tr3; +st214: + if ( ++p == pe ) + goto _test_eof214; +case 214: + if ( (*p) == 104 ) + goto st215; + goto tr3; +st215: + if ( ++p == pe ) + goto _test_eof215; +case 215: + if ( (*p) == 125 ) + goto st298; + goto tr3; +st298: + if ( ++p == pe ) + goto _test_eof298; +case 298: + goto tr365; +st216: + if ( ++p == pe ) + goto _test_eof216; +case 216: + if ( (*p) == 110 ) + goto st217; + goto tr3; +st217: + if ( ++p == pe ) + goto _test_eof217; +case 217: + if ( (*p) == 103 ) + goto st218; + goto tr3; +st218: + if ( ++p == pe ) + goto _test_eof218; +case 218: + if ( (*p) == 125 ) + goto st299; + goto tr3; +st299: + if ( ++p == pe ) + goto _test_eof299; +case 299: + goto tr366; +st219: + if ( ++p == pe ) + goto _test_eof219; +case 219: + if ( (*p) == 101 ) + goto st220; + goto tr3; +st220: + if ( ++p == pe ) + goto _test_eof220; +case 220: + if ( (*p) == 120 ) + goto st221; + goto tr3; +st221: + if ( ++p == pe ) + goto _test_eof221; +case 221: + if ( (*p) == 116 ) + goto st222; + goto tr3; +st222: + if ( ++p == pe ) + goto _test_eof222; +case 222: + if ( (*p) == 125 ) + goto st300; + goto tr3; +st300: + if ( ++p == pe ) + goto _test_eof300; +case 300: + goto tr367; +st223: + if ( ++p == pe ) + goto _test_eof223; +case 223: + switch( (*p) ) { + case 110: goto st224; + case 125: goto st249; + } + goto tr3; +st224: + if ( ++p == pe ) + goto _test_eof224; +case 224: + if ( (*p) == 100 ) + goto st225; + goto tr3; +st225: + if ( ++p == pe ) + goto _test_eof225; +case 225: + if ( (*p) == 101 ) + goto st226; + goto tr3; +st226: + if ( ++p == pe ) + goto _test_eof226; +case 226: + if ( (*p) == 114 ) + goto st227; + goto tr3; +st227: + if ( ++p == pe ) + goto _test_eof227; +case 227: + if ( (*p) == 108 ) + goto st228; + goto tr3; +st228: + if ( ++p == pe ) + goto _test_eof228; +case 228: + if ( (*p) == 105 ) + goto st229; + goto tr3; +st229: + if ( ++p == pe ) + goto _test_eof229; +case 229: + if ( (*p) == 110 ) + goto st230; + goto tr3; +st230: + if ( ++p == pe ) + goto _test_eof230; +case 230: + if ( (*p) == 101 ) + goto st11; + goto tr3; +st231: + if ( ++p == pe ) + goto _test_eof231; +case 231: + if ( (*p) == 104 ) + goto st232; + goto tr3; +st232: + if ( ++p == pe ) + goto _test_eof232; +case 232: + if ( (*p) == 105 ) + goto st233; + goto tr3; +st233: + if ( ++p == pe ) + goto _test_eof233; +case 233: + if ( (*p) == 116 ) + goto st234; + goto tr3; +st234: + if ( ++p == pe ) + goto _test_eof234; +case 234: + if ( (*p) == 101 ) + goto st235; + goto tr3; +st235: + if ( ++p == pe ) + goto _test_eof235; +case 235: + if ( (*p) == 125 ) + goto st301; + goto tr3; +st301: + if ( ++p == pe ) + goto _test_eof301; +case 301: + goto tr368; +st236: + if ( ++p == pe ) + goto _test_eof236; +case 236: + if ( (*p) == 101 ) + goto st237; + goto tr3; +st237: + if ( ++p == pe ) + goto _test_eof237; +case 237: + if ( (*p) == 108 ) + goto st238; + goto tr3; +st238: + if ( ++p == pe ) + goto _test_eof238; +case 238: + if ( (*p) == 108 ) + goto st239; + goto tr3; +st239: + if ( ++p == pe ) + goto _test_eof239; +case 239: + if ( (*p) == 111 ) + goto st240; + goto tr3; +st240: + if ( ++p == pe ) + goto _test_eof240; +case 240: + if ( (*p) == 119 ) + goto st241; + goto tr3; +st241: + if ( ++p == pe ) + goto _test_eof241; +case 241: + if ( (*p) == 125 ) + goto st302; + goto tr3; +st302: + if ( ++p == pe ) + goto _test_eof302; +case 302: + goto tr369; + } + _test_eof242: cs = 242; goto _test_eof; + _test_eof1: cs = 1; goto _test_eof; + _test_eof2: cs = 2; goto _test_eof; + _test_eof243: cs = 243; goto _test_eof; + _test_eof3: cs = 3; goto _test_eof; + _test_eof4: cs = 4; goto _test_eof; + _test_eof244: cs = 244; goto _test_eof; + _test_eof5: cs = 5; goto _test_eof; + _test_eof6: cs = 6; goto _test_eof; + _test_eof245: cs = 245; goto _test_eof; + _test_eof7: cs = 7; goto _test_eof; + _test_eof8: cs = 8; goto _test_eof; + _test_eof246: cs = 246; goto _test_eof; + _test_eof9: cs = 9; goto _test_eof; + _test_eof247: cs = 247; goto _test_eof; + _test_eof10: cs = 10; goto _test_eof; + _test_eof248: cs = 248; goto _test_eof; + _test_eof11: cs = 11; goto _test_eof; + _test_eof249: cs = 249; goto _test_eof; + _test_eof12: cs = 12; goto _test_eof; + _test_eof13: cs = 13; goto _test_eof; + _test_eof14: cs = 14; goto _test_eof; + _test_eof15: cs = 15; goto _test_eof; + _test_eof250: cs = 250; goto _test_eof; + _test_eof16: cs = 16; goto _test_eof; + _test_eof17: cs = 17; goto _test_eof; + _test_eof18: cs = 18; goto _test_eof; + _test_eof19: cs = 19; goto _test_eof; + _test_eof20: cs = 20; goto _test_eof; + _test_eof251: cs = 251; goto _test_eof; + _test_eof21: cs = 21; goto _test_eof; + _test_eof22: cs = 22; goto _test_eof; + _test_eof252: cs = 252; goto _test_eof; + _test_eof23: cs = 23; goto _test_eof; + _test_eof24: cs = 24; goto _test_eof; + _test_eof25: cs = 25; goto _test_eof; + _test_eof26: cs = 26; goto _test_eof; + _test_eof253: cs = 253; goto _test_eof; + _test_eof27: cs = 27; goto _test_eof; + _test_eof28: cs = 28; goto _test_eof; + _test_eof29: cs = 29; goto _test_eof; + _test_eof30: cs = 30; goto _test_eof; + _test_eof254: cs = 254; goto _test_eof; + _test_eof31: cs = 31; goto _test_eof; + _test_eof32: cs = 32; goto _test_eof; + _test_eof33: cs = 33; goto _test_eof; + _test_eof255: cs = 255; goto _test_eof; + _test_eof34: cs = 34; goto _test_eof; + _test_eof35: cs = 35; goto _test_eof; + _test_eof36: cs = 36; goto _test_eof; + _test_eof37: cs = 37; goto _test_eof; + _test_eof38: cs = 38; goto _test_eof; + _test_eof39: cs = 39; goto _test_eof; + _test_eof40: cs = 40; goto _test_eof; + _test_eof256: cs = 256; goto _test_eof; + _test_eof41: cs = 41; goto _test_eof; + _test_eof42: cs = 42; goto _test_eof; + _test_eof43: cs = 43; goto _test_eof; + _test_eof257: cs = 257; goto _test_eof; + _test_eof44: cs = 44; goto _test_eof; + _test_eof45: cs = 45; goto _test_eof; + _test_eof46: cs = 46; goto _test_eof; + _test_eof47: cs = 47; goto _test_eof; + _test_eof48: cs = 48; goto _test_eof; + _test_eof258: cs = 258; goto _test_eof; + _test_eof49: cs = 49; goto _test_eof; + _test_eof50: cs = 50; goto _test_eof; + _test_eof51: cs = 51; goto _test_eof; + _test_eof52: cs = 52; goto _test_eof; + _test_eof53: cs = 53; goto _test_eof; + _test_eof54: cs = 54; goto _test_eof; + _test_eof259: cs = 259; goto _test_eof; + _test_eof55: cs = 55; goto _test_eof; + _test_eof56: cs = 56; goto _test_eof; + _test_eof57: cs = 57; goto _test_eof; + _test_eof58: cs = 58; goto _test_eof; + _test_eof260: cs = 260; goto _test_eof; + _test_eof59: cs = 59; goto _test_eof; + _test_eof60: cs = 60; goto _test_eof; + _test_eof261: cs = 261; goto _test_eof; + _test_eof61: cs = 61; goto _test_eof; + _test_eof62: cs = 62; goto _test_eof; + _test_eof63: cs = 63; goto _test_eof; + _test_eof64: cs = 64; goto _test_eof; + _test_eof65: cs = 65; goto _test_eof; + _test_eof66: cs = 66; goto _test_eof; + _test_eof67: cs = 67; goto _test_eof; + _test_eof68: cs = 68; goto _test_eof; + _test_eof69: cs = 69; goto _test_eof; + _test_eof70: cs = 70; goto _test_eof; + _test_eof71: cs = 71; goto _test_eof; + _test_eof72: cs = 72; goto _test_eof; + _test_eof73: cs = 73; goto _test_eof; + _test_eof262: cs = 262; goto _test_eof; + _test_eof74: cs = 74; goto _test_eof; + _test_eof75: cs = 75; goto _test_eof; + _test_eof263: cs = 263; goto _test_eof; + _test_eof76: cs = 76; goto _test_eof; + _test_eof77: cs = 77; goto _test_eof; + _test_eof78: cs = 78; goto _test_eof; + _test_eof79: cs = 79; goto _test_eof; + _test_eof264: cs = 264; goto _test_eof; + _test_eof80: cs = 80; goto _test_eof; + _test_eof81: cs = 81; goto _test_eof; + _test_eof82: cs = 82; goto _test_eof; + _test_eof83: cs = 83; goto _test_eof; + _test_eof84: cs = 84; goto _test_eof; + _test_eof265: cs = 265; goto _test_eof; + _test_eof85: cs = 85; goto _test_eof; + _test_eof86: cs = 86; goto _test_eof; + _test_eof87: cs = 87; goto _test_eof; + _test_eof88: cs = 88; goto _test_eof; + _test_eof89: cs = 89; goto _test_eof; + _test_eof90: cs = 90; goto _test_eof; + _test_eof91: cs = 91; goto _test_eof; + _test_eof266: cs = 266; goto _test_eof; + _test_eof92: cs = 92; goto _test_eof; + _test_eof93: cs = 93; goto _test_eof; + _test_eof94: cs = 94; goto _test_eof; + _test_eof267: cs = 267; goto _test_eof; + _test_eof95: cs = 95; goto _test_eof; + _test_eof96: cs = 96; goto _test_eof; + _test_eof97: cs = 97; goto _test_eof; + _test_eof98: cs = 98; goto _test_eof; + _test_eof99: cs = 99; goto _test_eof; + _test_eof268: cs = 268; goto _test_eof; + _test_eof100: cs = 100; goto _test_eof; + _test_eof101: cs = 101; goto _test_eof; + _test_eof102: cs = 102; goto _test_eof; + _test_eof103: cs = 103; goto _test_eof; + _test_eof104: cs = 104; goto _test_eof; + _test_eof105: cs = 105; goto _test_eof; + _test_eof269: cs = 269; goto _test_eof; + _test_eof270: cs = 270; goto _test_eof; + _test_eof271: cs = 271; goto _test_eof; + _test_eof106: cs = 106; goto _test_eof; + _test_eof107: cs = 107; goto _test_eof; + _test_eof108: cs = 108; goto _test_eof; + _test_eof272: cs = 272; goto _test_eof; + _test_eof109: cs = 109; goto _test_eof; + _test_eof273: cs = 273; goto _test_eof; + _test_eof110: cs = 110; goto _test_eof; + _test_eof111: cs = 111; goto _test_eof; + _test_eof112: cs = 112; goto _test_eof; + _test_eof113: cs = 113; goto _test_eof; + _test_eof114: cs = 114; goto _test_eof; + _test_eof274: cs = 274; goto _test_eof; + _test_eof115: cs = 115; goto _test_eof; + _test_eof116: cs = 116; goto _test_eof; + _test_eof117: cs = 117; goto _test_eof; + _test_eof275: cs = 275; goto _test_eof; + _test_eof118: cs = 118; goto _test_eof; + _test_eof119: cs = 119; goto _test_eof; + _test_eof120: cs = 120; goto _test_eof; + _test_eof121: cs = 121; goto _test_eof; + _test_eof122: cs = 122; goto _test_eof; + _test_eof123: cs = 123; goto _test_eof; + _test_eof276: cs = 276; goto _test_eof; + _test_eof124: cs = 124; goto _test_eof; + _test_eof125: cs = 125; goto _test_eof; + _test_eof277: cs = 277; goto _test_eof; + _test_eof126: cs = 126; goto _test_eof; + _test_eof127: cs = 127; goto _test_eof; + _test_eof128: cs = 128; goto _test_eof; + _test_eof129: cs = 129; goto _test_eof; + _test_eof278: cs = 278; goto _test_eof; + _test_eof130: cs = 130; goto _test_eof; + _test_eof131: cs = 131; goto _test_eof; + _test_eof132: cs = 132; goto _test_eof; + _test_eof133: cs = 133; goto _test_eof; + _test_eof134: cs = 134; goto _test_eof; + _test_eof135: cs = 135; goto _test_eof; + _test_eof136: cs = 136; goto _test_eof; + _test_eof137: cs = 137; goto _test_eof; + _test_eof279: cs = 279; goto _test_eof; + _test_eof138: cs = 138; goto _test_eof; + _test_eof139: cs = 139; goto _test_eof; + _test_eof140: cs = 140; goto _test_eof; + _test_eof141: cs = 141; goto _test_eof; + _test_eof142: cs = 142; goto _test_eof; + _test_eof143: cs = 143; goto _test_eof; + _test_eof144: cs = 144; goto _test_eof; + _test_eof280: cs = 280; goto _test_eof; + _test_eof145: cs = 145; goto _test_eof; + _test_eof146: cs = 146; goto _test_eof; + _test_eof147: cs = 147; goto _test_eof; + _test_eof281: cs = 281; goto _test_eof; + _test_eof148: cs = 148; goto _test_eof; + _test_eof149: cs = 149; goto _test_eof; + _test_eof150: cs = 150; goto _test_eof; + _test_eof282: cs = 282; goto _test_eof; + _test_eof151: cs = 151; goto _test_eof; + _test_eof152: cs = 152; goto _test_eof; + _test_eof283: cs = 283; goto _test_eof; + _test_eof153: cs = 153; goto _test_eof; + _test_eof284: cs = 284; goto _test_eof; + _test_eof154: cs = 154; goto _test_eof; + _test_eof285: cs = 285; goto _test_eof; + _test_eof155: cs = 155; goto _test_eof; + _test_eof286: cs = 286; goto _test_eof; + _test_eof156: cs = 156; goto _test_eof; + _test_eof287: cs = 287; goto _test_eof; + _test_eof157: cs = 157; goto _test_eof; + _test_eof288: cs = 288; goto _test_eof; + _test_eof158: cs = 158; goto _test_eof; + _test_eof159: cs = 159; goto _test_eof; + _test_eof160: cs = 160; goto _test_eof; + _test_eof289: cs = 289; goto _test_eof; + _test_eof161: cs = 161; goto _test_eof; + _test_eof162: cs = 162; goto _test_eof; + _test_eof163: cs = 163; goto _test_eof; + _test_eof164: cs = 164; goto _test_eof; + _test_eof165: cs = 165; goto _test_eof; + _test_eof166: cs = 166; goto _test_eof; + _test_eof167: cs = 167; goto _test_eof; + _test_eof290: cs = 290; goto _test_eof; + _test_eof168: cs = 168; goto _test_eof; + _test_eof169: cs = 169; goto _test_eof; + _test_eof170: cs = 170; goto _test_eof; + _test_eof291: cs = 291; goto _test_eof; + _test_eof171: cs = 171; goto _test_eof; + _test_eof172: cs = 172; goto _test_eof; + _test_eof173: cs = 173; goto _test_eof; + _test_eof174: cs = 174; goto _test_eof; + _test_eof175: cs = 175; goto _test_eof; + _test_eof176: cs = 176; goto _test_eof; + _test_eof177: cs = 177; goto _test_eof; + _test_eof178: cs = 178; goto _test_eof; + _test_eof179: cs = 179; goto _test_eof; + _test_eof180: cs = 180; goto _test_eof; + _test_eof181: cs = 181; goto _test_eof; + _test_eof182: cs = 182; goto _test_eof; + _test_eof292: cs = 292; goto _test_eof; + _test_eof183: cs = 183; goto _test_eof; + _test_eof184: cs = 184; goto _test_eof; + _test_eof185: cs = 185; goto _test_eof; + _test_eof293: cs = 293; goto _test_eof; + _test_eof186: cs = 186; goto _test_eof; + _test_eof187: cs = 187; goto _test_eof; + _test_eof188: cs = 188; goto _test_eof; + _test_eof294: cs = 294; goto _test_eof; + _test_eof189: cs = 189; goto _test_eof; + _test_eof190: cs = 190; goto _test_eof; + _test_eof191: cs = 191; goto _test_eof; + _test_eof192: cs = 192; goto _test_eof; + _test_eof193: cs = 193; goto _test_eof; + _test_eof295: cs = 295; goto _test_eof; + _test_eof194: cs = 194; goto _test_eof; + _test_eof195: cs = 195; goto _test_eof; + _test_eof196: cs = 196; goto _test_eof; + _test_eof197: cs = 197; goto _test_eof; + _test_eof198: cs = 198; goto _test_eof; + _test_eof199: cs = 199; goto _test_eof; + _test_eof200: cs = 200; goto _test_eof; + _test_eof296: cs = 296; goto _test_eof; + _test_eof201: cs = 201; goto _test_eof; + _test_eof202: cs = 202; goto _test_eof; + _test_eof203: cs = 203; goto _test_eof; + _test_eof297: cs = 297; goto _test_eof; + _test_eof204: cs = 204; goto _test_eof; + _test_eof205: cs = 205; goto _test_eof; + _test_eof206: cs = 206; goto _test_eof; + _test_eof207: cs = 207; goto _test_eof; + _test_eof208: cs = 208; goto _test_eof; + _test_eof209: cs = 209; goto _test_eof; + _test_eof210: cs = 210; goto _test_eof; + _test_eof211: cs = 211; goto _test_eof; + _test_eof212: cs = 212; goto _test_eof; + _test_eof213: cs = 213; goto _test_eof; + _test_eof214: cs = 214; goto _test_eof; + _test_eof215: cs = 215; goto _test_eof; + _test_eof298: cs = 298; goto _test_eof; + _test_eof216: cs = 216; goto _test_eof; + _test_eof217: cs = 217; goto _test_eof; + _test_eof218: cs = 218; goto _test_eof; + _test_eof299: cs = 299; goto _test_eof; + _test_eof219: cs = 219; goto _test_eof; + _test_eof220: cs = 220; goto _test_eof; + _test_eof221: cs = 221; goto _test_eof; + _test_eof222: cs = 222; goto _test_eof; + _test_eof300: cs = 300; goto _test_eof; + _test_eof223: cs = 223; goto _test_eof; + _test_eof224: cs = 224; goto _test_eof; + _test_eof225: cs = 225; goto _test_eof; + _test_eof226: cs = 226; goto _test_eof; + _test_eof227: cs = 227; goto _test_eof; + _test_eof228: cs = 228; goto _test_eof; + _test_eof229: cs = 229; goto _test_eof; + _test_eof230: cs = 230; goto _test_eof; + _test_eof231: cs = 231; goto _test_eof; + _test_eof232: cs = 232; goto _test_eof; + _test_eof233: cs = 233; goto _test_eof; + _test_eof234: cs = 234; goto _test_eof; + _test_eof235: cs = 235; goto _test_eof; + _test_eof301: cs = 301; goto _test_eof; + _test_eof236: cs = 236; goto _test_eof; + _test_eof237: cs = 237; goto _test_eof; + _test_eof238: cs = 238; goto _test_eof; + _test_eof239: cs = 239; goto _test_eof; + _test_eof240: cs = 240; goto _test_eof; + _test_eof241: cs = 241; goto _test_eof; + _test_eof302: cs = 302; goto _test_eof; + + _test_eof: {} + if ( p == eof ) + { + switch ( cs ) { + case 243: goto tr295; + case 3: goto tr3; + case 4: goto tr3; + case 244: goto tr311; + case 5: goto tr3; + case 6: goto tr3; + case 245: goto tr312; + case 7: goto tr3; + case 8: goto tr3; + case 246: goto tr313; + case 9: goto tr3; + case 247: goto tr314; + case 10: goto tr3; + case 248: goto tr315; + case 11: goto tr3; + case 249: goto tr316; + case 12: goto tr3; + case 13: goto tr3; + case 14: goto tr3; + case 15: goto tr3; + case 250: goto tr317; + case 16: goto tr3; + case 17: goto tr3; + case 18: goto tr3; + case 19: goto tr3; + case 20: goto tr3; + case 251: goto tr318; + case 21: goto tr3; + case 22: goto tr3; + case 252: goto tr319; + case 23: goto tr3; + case 24: goto tr3; + case 25: goto tr3; + case 26: goto tr3; + case 253: goto tr320; + case 27: goto tr3; + case 28: goto tr3; + case 29: goto tr3; + case 30: goto tr3; + case 254: goto tr321; + case 31: goto tr3; + case 32: goto tr3; + case 33: goto tr3; + case 255: goto tr322; + case 34: goto tr3; + case 35: goto tr3; + case 36: goto tr3; + case 37: goto tr3; + case 38: goto tr3; + case 39: goto tr3; + case 40: goto tr3; + case 256: goto tr323; + case 41: goto tr3; + case 42: goto tr3; + case 43: goto tr3; + case 257: goto tr324; + case 44: goto tr3; + case 45: goto tr3; + case 46: goto tr3; + case 47: goto tr3; + case 48: goto tr3; + case 258: goto tr325; + case 49: goto tr3; + case 50: goto tr3; + case 51: goto tr3; + case 52: goto tr3; + case 53: goto tr3; + case 54: goto tr3; + case 259: goto tr326; + case 55: goto tr3; + case 56: goto tr3; + case 57: goto tr3; + case 58: goto tr3; + case 260: goto tr327; + case 59: goto tr3; + case 60: goto tr3; + case 261: goto tr328; + case 61: goto tr3; + case 62: goto tr3; + case 63: goto tr3; + case 64: goto tr3; + case 65: goto tr3; + case 66: goto tr3; + case 67: goto tr3; + case 68: goto tr3; + case 69: goto tr3; + case 70: goto tr3; + case 71: goto tr3; + case 72: goto tr3; + case 73: goto tr3; + case 262: goto tr329; + case 74: goto tr3; + case 75: goto tr3; + case 263: goto tr330; + case 76: goto tr3; + case 77: goto tr3; + case 78: goto tr3; + case 79: goto tr3; + case 264: goto tr331; + case 80: goto tr3; + case 81: goto tr3; + case 82: goto tr3; + case 83: goto tr3; + case 84: goto tr3; + case 265: goto tr332; + case 85: goto tr3; + case 86: goto tr3; + case 87: goto tr3; + case 88: goto tr3; + case 89: goto tr3; + case 90: goto tr3; + case 91: goto tr3; + case 266: goto tr333; + case 92: goto tr3; + case 93: goto tr3; + case 94: goto tr3; + case 267: goto tr334; + case 95: goto tr3; + case 96: goto tr3; + case 97: goto tr3; + case 98: goto tr3; + case 99: goto tr3; + case 268: goto tr335; + case 100: goto tr3; + case 101: goto tr3; + case 102: goto tr3; + case 103: goto tr3; + case 104: goto tr3; + case 105: goto tr3; + case 269: goto tr336; + case 270: goto tr337; + case 271: goto tr338; + case 106: goto tr3; + case 107: goto tr3; + case 108: goto tr3; + case 272: goto tr339; + case 109: goto tr3; + case 273: goto tr340; + case 110: goto tr3; + case 111: goto tr3; + case 112: goto tr3; + case 113: goto tr3; + case 114: goto tr3; + case 274: goto tr341; + case 115: goto tr3; + case 116: goto tr3; + case 117: goto tr3; + case 275: goto tr342; + case 118: goto tr3; + case 119: goto tr3; + case 120: goto tr3; + case 121: goto tr3; + case 122: goto tr3; + case 123: goto tr3; + case 276: goto tr343; + case 124: goto tr3; + case 125: goto tr3; + case 277: goto tr344; + case 126: goto tr3; + case 127: goto tr3; + case 128: goto tr3; + case 129: goto tr3; + case 278: goto tr345; + case 130: goto tr3; + case 131: goto tr3; + case 132: goto tr3; + case 133: goto tr3; + case 134: goto tr3; + case 135: goto tr3; + case 136: goto tr3; + case 137: goto tr3; + case 279: goto tr346; + case 138: goto tr3; + case 139: goto tr3; + case 140: goto tr3; + case 141: goto tr3; + case 142: goto tr3; + case 143: goto tr3; + case 144: goto tr3; + case 280: goto tr347; + case 145: goto tr3; + case 146: goto tr3; + case 147: goto tr3; + case 281: goto tr348; + case 148: goto tr3; + case 149: goto tr3; + case 150: goto tr3; + case 282: goto tr349; + case 151: goto tr3; + case 152: goto tr3; + case 283: goto tr350; + case 153: goto tr3; + case 284: goto tr351; + case 154: goto tr3; + case 285: goto tr352; + case 155: goto tr3; + case 286: goto tr353; + case 156: goto tr3; + case 287: goto tr354; + case 157: goto tr3; + case 288: goto tr355; + case 158: goto tr3; + case 159: goto tr3; + case 160: goto tr3; + case 289: goto tr356; + case 161: goto tr3; + case 162: goto tr3; + case 163: goto tr3; + case 164: goto tr3; + case 165: goto tr3; + case 166: goto tr3; + case 167: goto tr3; + case 290: goto tr357; + case 168: goto tr3; + case 169: goto tr3; + case 170: goto tr3; + case 291: goto tr358; + case 171: goto tr3; + case 172: goto tr3; + case 173: goto tr3; + case 174: goto tr3; + case 175: goto tr3; + case 176: goto tr3; + case 177: goto tr3; + case 178: goto tr3; + case 179: goto tr3; + case 180: goto tr3; + case 181: goto tr3; + case 182: goto tr3; + case 292: goto tr359; + case 183: goto tr3; + case 184: goto tr3; + case 185: goto tr3; + case 293: goto tr360; + case 186: goto tr3; + case 187: goto tr3; + case 188: goto tr3; + case 294: goto tr361; + case 189: goto tr3; + case 190: goto tr3; + case 191: goto tr3; + case 192: goto tr3; + case 193: goto tr3; + case 295: goto tr362; + case 194: goto tr3; + case 195: goto tr3; + case 196: goto tr3; + case 197: goto tr3; + case 198: goto tr3; + case 199: goto tr3; + case 200: goto tr3; + case 296: goto tr363; + case 201: goto tr3; + case 202: goto tr3; + case 203: goto tr3; + case 297: goto tr364; + case 204: goto tr3; + case 205: goto tr3; + case 206: goto tr3; + case 207: goto tr3; + case 208: goto tr3; + case 209: goto tr3; + case 210: goto tr3; + case 211: goto tr3; + case 212: goto tr3; + case 213: goto tr3; + case 214: goto tr3; + case 215: goto tr3; + case 298: goto tr365; + case 216: goto tr3; + case 217: goto tr3; + case 218: goto tr3; + case 299: goto tr366; + case 219: goto tr3; + case 220: goto tr3; + case 221: goto tr3; + case 222: goto tr3; + case 300: goto tr367; + case 223: goto tr3; + case 224: goto tr3; + case 225: goto tr3; + case 226: goto tr3; + case 227: goto tr3; + case 228: goto tr3; + case 229: goto tr3; + case 230: goto tr3; + case 231: goto tr3; + case 232: goto tr3; + case 233: goto tr3; + case 234: goto tr3; + case 235: goto tr3; + case 301: goto tr368; + case 236: goto tr3; + case 237: goto tr3; + case 238: goto tr3; + case 239: goto tr3; + case 240: goto tr3; + case 241: goto tr3; + case 302: goto tr369; + } + } + + _out: {} + } + +#line 955 "src/tokens.rl" + + result.cursor_col = parser.cursor_col; + result.cursor_row = parser.cursor_row; + result.has_cursor = parser.has_cursor; + result.final_col = parser.visual_col; + result.final_row = parser.visual_row; + return result; +} + +zstr expand_tokens(const char *text) { + TokenExpansion result = expand_tokens_with_cursor(text); + return result.expanded; +} + +void zstr_expand_to(FILE *stream, const char *text) { + if (!text || !*text) return; + + Z_CLEANUP(zstr_free) zstr expanded = expand_tokens(text); + fwrite(zstr_cstr(&expanded), 1, zstr_len(&expanded), stream); +} + +void token_expansion_render(FILE *stream, TokenExpansion *te) { + if (!te) return; + + /* Write the expanded content */ + fwrite(zstr_cstr(&te->expanded), 1, zstr_len(&te->expanded), stream); + + /* If cursor was marked, clear to end of line and position cursor */ + if (te->has_cursor) { + /* Clear from current position to end of line (gives cursor room) */ + if (!zstr_no_colors) { + fprintf(stream, "\033[K"); + } + + /* Position cursor at marked location and show it */ + if (!zstr_no_colors && te->cursor_row > 0 && te->cursor_col > 0) { + fprintf(stream, "\033[%d;%dH\033[?25h", te->cursor_row, te->cursor_col); + } + } +} diff --git a/src/tokens.h b/src/tokens.h new file mode 100644 index 0000000..11a4c5e --- /dev/null +++ b/src/tokens.h @@ -0,0 +1,160 @@ +/* + * Token expansion system - Header + * + * Provides stack-based ANSI escape code generation from markup tokens. + * Generated by Ragel from tokens.rl + * + * Token Types: + * + * SEMANTIC TOKENS (composite styles, configurable via globals): + * {b} - Bold only (same as {strong}) + * {highlight} - Bold + yellow (for highlighting, e.g., fuzzy matches) + * {h1}-{h6} - Heading styles (default: h1=orange, h2=blue, h3-h6=white) + * {strong} - Strong/bold text + * {dim} - Gray foreground (bright black) + * {section} - Bold + dark gray background, for selection highlighting + * {danger} - Dark red background (for warnings/deletions) + * {strike} - Strikethrough text (alias for {strikethrough}) + * {text} - Full reset (alias for {reset}) + * + * ATTRIBUTE TOKENS: + * {bold}, {B} - Bold text + * {italic}, {I}, {i} - Italic text + * {underline}, {U}, {u} - Underlined text + * {reverse} - Reverse/inverse video + * {strikethrough} - Strikethrough text + * {bright} - Brighten current foreground color + * + * FOREGROUND COLORS: + * {black}, {red}, {green}, {yellow}, {blue}, {magenta}, {cyan}, {white} + * {gray}, {grey} - Gray (bright black) + * {bright:COLOR} - Bright variant (e.g., {bright:red}) + * {fg:N} - 256-color foreground (N = 0-255) + * + * BACKGROUND COLORS: + * {bg:COLOR} - Background color (e.g., {bg:red}) + * {bg:N} - 256-color background (N = 0-255) + * + * RESET/POP TOKENS: + * {/} - Pop one level, restore previous style + * {reset} - Full reset, clear stack + * {/fg} - Reset foreground color only + * {/bg} - Reset background color only + * {/b} - Legacy: same as {/} + * {/section} - Legacy: same as {/} + * {/strike} - Legacy: same as {/} + * + * CONTROL TOKENS (terminal control sequences): + * {clr} - Clear to end of line (ESC[K) + * {cls} - Clear to end of screen (ESC[J) + * {home} - Move cursor to home position (ESC[H) + * {hide} - Hide cursor (ESC[?25l) + * {show} - Show cursor (ESC[?25h) + * {goto:row,col} - Move cursor to position (ESC[row;colH) + * {goto_cursor} - Move cursor to {cursor} marker position + * + * SPECIAL: + * {cursor} - Mark cursor position (for cursor tracking) + * + * Stack Semantics: + * Each style-setting token pushes its previous state onto a stack. + * {/} pops one level, restoring the previous state. + * Composite tokens (like {highlight}) push multiple attrs and a composite marker, + * so a single {/} restores all of them at once. + * + * Auto-reset: + * All styles are automatically reset at the end of each line (before newline). + * This ensures clean line boundaries without trailing ANSI codes. + * + * Redundancy avoidance: + * Repeated identical styles don't emit redundant codes. + * e.g., "{dim}{dim}{dim}x" outputs same as "{dim}x" + * + * Example: + * "Normal {highlight}bold yellow {red}now red{/} back to yellow{/} normal" + * "Nested: {bold}{italic}bold+italic{/}just bold{/}normal" + */ + +#ifndef TOKENS_H +#define TOKENS_H + +#include "zstr.h" +#include + +/* + * Global style configuration for semantic tokens. + * These can be modified at runtime to customize appearance. + * Format: ANSI SGR parameter string (without leading \033[ or trailing m) + */ +extern const char *token_style_h1; /* Default: "1m\033[38;5;214" (bold+orange) */ +extern const char *token_style_h2; /* Default: "1;34" (bold+blue) */ +extern const char *token_style_h3; /* Default: "1;37" (bold+white) */ +extern const char *token_style_h4; /* Default: "1;37" (bold+white) */ +extern const char *token_style_h5; /* Default: "1;37" (bold+white) */ +extern const char *token_style_h6; /* Default: "1;37" (bold+white) */ +extern const char *token_style_b; /* Default: "1" (bold) */ +extern const char *token_style_strong; /* Default: "1" (bold) */ +extern const char *token_style_highlight; /* Default: "1;33" (bold+yellow) */ +extern const char *token_style_dim; /* Default: "90" (gray) */ +extern const char *token_style_danger; /* Default: "48;5;52" (dark red bg) */ + +/* Global flags for controlling token expansion */ +extern bool zstr_disable_token_expansion; /* If true, pass through unchanged */ +extern bool zstr_no_colors; /* If true, tokens expand to empty strings */ + +/* Result of token expansion with state tracking */ +typedef struct { + zstr expanded; /* The expanded string with ANSI codes */ + int cursor_col; /* Visual column of {cursor} marker (1-indexed), or -1 if not found */ + int cursor_row; /* Visual row of {cursor} marker (1-indexed), or -1 if not found */ + bool has_cursor; /* True if {cursor} was found in the input */ + int final_col; /* Final visual column after expansion */ + int final_row; /* Final visual row after expansion */ +} TokenExpansion; + +/* + * Core expansion functions (operate on C strings) + */ + +/* Expand tokens, returning expanded string and state */ +TokenExpansion expand_tokens_with_cursor(const char *text); + +/* Expand tokens, returning just the expanded string */ +zstr expand_tokens(const char *text); + +/* Free a TokenExpansion (just frees the expanded zstr) */ +static inline void token_expansion_free(TokenExpansion *te) { + zstr_free(&te->expanded); +} + +/* + * zstr wrapper functions (for convenience when working with zstr) + */ + +/* Expand tokens from C string, returning expanded string and state */ +static inline TokenExpansion zstr_expand_tokens_with_cursor(const char *text) { + return expand_tokens_with_cursor(text); +} + +/* Expand tokens from C string, returning just the expanded string */ +static inline zstr zstr_expand_tokens(const char *text) { + return expand_tokens(text); +} + +/* + * Direct output functions - expand tokens and write directly to stream + */ + +#include + +/* Expand tokens and write to FILE* stream (e.g., stdout, stderr) */ +void zstr_expand_to(FILE *stream, const char *text); + +/* + * Render a TokenExpansion to a stream. + * If has_cursor is true, appends {goto_cursor}{show} and clears to end of line. + * This provides a complete render with proper cursor positioning. + */ +void token_expansion_render(FILE *stream, TokenExpansion *te); + +#endif /* TOKENS_H */ diff --git a/src/tokens.rl b/src/tokens.rl new file mode 100644 index 0000000..c5c2c2b --- /dev/null +++ b/src/tokens.rl @@ -0,0 +1,994 @@ +/* + * Token expansion state machine - Ragel grammar + * + * This provides a stack-based token system for ANSI escape code generation. + * Tokens like {b}, {dim}, {red} push state, and {/} pops to restore. + * + * Generated with: ragel -C -G2 tokens.rl -o tokens.c + */ + +#include "tokens.h" +#include "zstr.h" +#include +#include +#include + +/* Maximum nesting depth for style stack */ +#define MAX_STACK_DEPTH 32 + +/* + * Global style configuration for semantic tokens. + * These can be overridden at runtime to customize the appearance. + * Format: ANSI escape sequence string (without the leading \033[) + */ +const char *token_style_h1 = "1m\033[38;5;214"; /* Bold + orange (256-color 214) */ +const char *token_style_h2 = "1;34"; /* Bold + blue */ +const char *token_style_h3 = "1;37"; /* Bold + white */ +const char *token_style_h4 = "1;37"; /* Bold + white */ +const char *token_style_h5 = "1;37"; /* Bold + white */ +const char *token_style_h6 = "1;37"; /* Bold + white */ +const char *token_style_b = "1"; /* Bold (same as strong) */ +const char *token_style_strong = "1"; /* Bold */ +const char *token_style_highlight = "1;33"; /* Bold + yellow */ +const char *token_style_dim = "90"; /* Gray (bright black) */ +const char *token_style_danger = "48;5;52"; /* Dark red bg */ + +/* Style attribute types */ +typedef enum { + ATTR_NONE = 0, + ATTR_BOLD, + ATTR_DIM, + ATTR_ITALIC, + ATTR_UNDERLINE, + ATTR_REVERSE, + ATTR_STRIKETHROUGH, + ATTR_FG, /* Foreground color */ + ATTR_BG, /* Background color */ + ATTR_COMPOSITE, /* Multiple attributes (for semantic tokens) */ +} AttrType; + +/* Represents a single attribute value */ +typedef struct { + AttrType type; + int value; /* For colors: ANSI code, for bools: 0/1 */ +} AttrValue; + +/* Stack entry - stores what to restore when popping */ +typedef struct { + AttrType type; + int prev_value; + int count; /* For composite: how many individual attrs were pushed */ +} StackEntry; + +/* Parser state */ +typedef struct { + zstr *out; /* Output buffer */ + StackEntry stack[MAX_STACK_DEPTH]; + int stack_depth; + int cursor_col; /* Visual column of {cursor} (1-indexed), -1 if no cursor */ + int cursor_row; /* Visual row of {cursor} (1-indexed), -1 if no cursor */ + bool has_cursor; /* True if {cursor} was encountered */ + int visual_col; /* Current visual column */ + int visual_row; /* Current visual row */ + bool no_colors; /* If true, don't emit ANSI codes */ + bool disabled; /* If true, pass through without expansion */ + + /* Desired style state (what tokens request) */ + int fg_color; /* 0 = default, else ANSI code */ + int bg_color; + bool bold; + bool dim; + bool italic; + bool underline; + bool reverse; + bool strikethrough; + + /* Emitted style state (what terminal currently has) */ + int emitted_fg; + int emitted_bg; + bool emitted_bold; + bool emitted_dim; + bool emitted_italic; + bool emitted_underline; + bool emitted_reverse; + bool emitted_strikethrough; + bool dirty; /* True if desired != emitted */ +} TokenParser; + +/* ANSI escape code helpers */ +static void emit_ansi(TokenParser *p, const char *code) { + if (!p->no_colors) { + zstr_cat(p->out, code); + } +} + +static void emit_ansi_num(TokenParser *p, const char *prefix, int n, const char *suffix) { + if (!p->no_colors) { + char buf[32]; + snprintf(buf, sizeof(buf), "%s%d%s", prefix, n, suffix); + zstr_cat(p->out, buf); + } +} + +/* Mark state as dirty (deferred emission) */ +static void mark_dirty(TokenParser *p) { + p->dirty = true; +} + +/* Sync emitted state with desired state - emit codes only when outputting a character */ +static void sync_styles(TokenParser *p) { + if (!p->dirty || p->no_colors) return; + + /* Check if we need a full reset (going back to default state) */ + bool need_reset = false; + + /* If any attribute is turning OFF, we need reset then re-apply actives */ + if ((p->emitted_bold && !p->bold) || + (p->emitted_dim && !p->dim) || + (p->emitted_italic && !p->italic) || + (p->emitted_underline && !p->underline) || + (p->emitted_reverse && !p->reverse) || + (p->emitted_strikethrough && !p->strikethrough) || + (p->emitted_fg != 0 && p->fg_color == 0) || + (p->emitted_bg != 0 && p->bg_color == 0)) { + need_reset = true; + } + + if (need_reset) { + /* Reset and re-apply all active styles */ + zstr_cat(p->out, "\033[0"); + if (p->bold) zstr_cat(p->out, ";1"); + if (p->dim) zstr_cat(p->out, ";2"); + if (p->italic) zstr_cat(p->out, ";3"); + if (p->underline) zstr_cat(p->out, ";4"); + if (p->reverse) zstr_cat(p->out, ";7"); + if (p->strikethrough) zstr_cat(p->out, ";9"); + if (p->fg_color != 0) { + char buf[20]; + if (p->fg_color >= 1000 && p->fg_color < 2000) { + /* 256-color foreground */ + snprintf(buf, sizeof(buf), ";38;5;%d", p->fg_color - 1000); + } else { + snprintf(buf, sizeof(buf), ";%d", p->fg_color); + } + zstr_cat(p->out, buf); + } + if (p->bg_color != 0) { + char buf[20]; + if (p->bg_color >= 2000) { + /* 256-color background */ + snprintf(buf, sizeof(buf), ";48;5;%d", p->bg_color - 2000); + } else { + snprintf(buf, sizeof(buf), ";%d", p->bg_color); + } + zstr_cat(p->out, buf); + } + zstr_cat(p->out, "m"); + } else { + /* Apply only changed attributes (turning ON) */ + bool first = true; + char buf[64]; + buf[0] = '\0'; + + if (p->bold && !p->emitted_bold) { + strcat(buf, first ? "1" : ";1"); first = false; + } + if (p->dim && !p->emitted_dim) { + strcat(buf, first ? "2" : ";2"); first = false; + } + if (p->italic && !p->emitted_italic) { + strcat(buf, first ? "3" : ";3"); first = false; + } + if (p->underline && !p->emitted_underline) { + strcat(buf, first ? "4" : ";4"); first = false; + } + if (p->reverse && !p->emitted_reverse) { + strcat(buf, first ? "7" : ";7"); first = false; + } + if (p->strikethrough && !p->emitted_strikethrough) { + strcat(buf, first ? "9" : ";9"); first = false; + } + if (p->fg_color != p->emitted_fg && p->fg_color != 0) { + char tmp[20]; + if (p->fg_color >= 1000 && p->fg_color < 2000) { + snprintf(tmp, sizeof(tmp), "%s38;5;%d", first ? "" : ";", p->fg_color - 1000); + } else { + snprintf(tmp, sizeof(tmp), "%s%d", first ? "" : ";", p->fg_color); + } + strcat(buf, tmp); first = false; + } + if (p->bg_color != p->emitted_bg && p->bg_color != 0) { + char tmp[20]; + if (p->bg_color >= 2000) { + snprintf(tmp, sizeof(tmp), "%s48;5;%d", first ? "" : ";", p->bg_color - 2000); + } else { + snprintf(tmp, sizeof(tmp), "%s%d", first ? "" : ";", p->bg_color); + } + strcat(buf, tmp); first = false; + } + + if (!first) { + zstr_cat(p->out, "\033["); + zstr_cat(p->out, buf); + zstr_cat(p->out, "m"); + } + } + + /* Update emitted state to match desired */ + p->emitted_bold = p->bold; + p->emitted_dim = p->dim; + p->emitted_italic = p->italic; + p->emitted_underline = p->underline; + p->emitted_reverse = p->reverse; + p->emitted_strikethrough = p->strikethrough; + p->emitted_fg = p->fg_color; + p->emitted_bg = p->bg_color; + p->dirty = false; +} + +/* Push a restore entry onto the stack */ +static void push_attr(TokenParser *p, AttrType type, int prev_value) { + if (p->stack_depth < MAX_STACK_DEPTH) { + p->stack[p->stack_depth].type = type; + p->stack[p->stack_depth].prev_value = prev_value; + p->stack[p->stack_depth].count = 1; + p->stack_depth++; + } +} + +/* Push a composite (multiple attrs at once) */ +static void push_composite(TokenParser *p, int count) { + if (p->stack_depth < MAX_STACK_DEPTH) { + p->stack[p->stack_depth].type = ATTR_COMPOSITE; + p->stack[p->stack_depth].prev_value = 0; + p->stack[p->stack_depth].count = count; + p->stack_depth++; + } +} + +/* Restore an attribute to its previous value (deferred - just updates state) */ +static void restore_attr(TokenParser *p, AttrType type, int prev_value) { + switch (type) { + case ATTR_BOLD: + p->bold = prev_value; + break; + case ATTR_DIM: + p->dim = prev_value; + break; + case ATTR_ITALIC: + p->italic = prev_value; + break; + case ATTR_UNDERLINE: + p->underline = prev_value; + break; + case ATTR_REVERSE: + p->reverse = prev_value; + break; + case ATTR_STRIKETHROUGH: + p->strikethrough = prev_value; + break; + case ATTR_FG: + p->fg_color = prev_value; + break; + case ATTR_BG: + p->bg_color = prev_value; + break; + default: + break; + } +} + +/* Pop one entry from the stack and restore */ +static void pop_style(TokenParser *p) { + if (p->stack_depth > 0) { + p->stack_depth--; + StackEntry *e = &p->stack[p->stack_depth]; + + if (e->type == ATTR_COMPOSITE) { + /* Pop multiple individual entries */ + for (int i = 0; i < e->count && p->stack_depth > 0; i++) { + p->stack_depth--; + StackEntry *inner = &p->stack[p->stack_depth]; + restore_attr(p, inner->type, inner->prev_value); + } + } else { + restore_attr(p, e->type, e->prev_value); + } + mark_dirty(p); + } +} + +/* Check if any styles are active */ +static bool has_active_styles(TokenParser *p) { + return p->bold || p->dim || p->italic || p->underline || + p->reverse || p->strikethrough || p->fg_color != 0 || p->bg_color != 0; +} + +/* Reset all styles and clear stack (deferred - just updates state) */ +static void reset_all(TokenParser *p) { + p->fg_color = 0; + p->bg_color = 0; + p->bold = false; + p->dim = false; + p->italic = false; + p->underline = false; + p->reverse = false; + p->strikethrough = false; + p->stack_depth = 0; + mark_dirty(p); +} + +/* Reset all styles at newline - emits immediately since newline follows */ +static void reset_line_styles(TokenParser *p) { + /* Check if emitted state has any active styles */ + if (p->emitted_bold || p->emitted_dim || p->emitted_italic || + p->emitted_underline || p->emitted_reverse || p->emitted_strikethrough || + p->emitted_fg != 0 || p->emitted_bg != 0) { + emit_ansi(p, "\033[0m"); + /* Update emitted state */ + p->emitted_bold = false; + p->emitted_dim = false; + p->emitted_italic = false; + p->emitted_underline = false; + p->emitted_reverse = false; + p->emitted_strikethrough = false; + p->emitted_fg = 0; + p->emitted_bg = 0; + } + /* Reset desired state too */ + p->fg_color = 0; + p->bg_color = 0; + p->bold = false; + p->dim = false; + p->italic = false; + p->underline = false; + p->reverse = false; + p->strikethrough = false; + p->dirty = false; + /* Keep stack_depth as-is so {/} still works across lines if needed */ +} + +/* Style application functions - deferred emission (just update state) */ +static void apply_bold(TokenParser *p) { + push_attr(p, ATTR_BOLD, p->bold); + p->bold = true; + mark_dirty(p); +} + +static void apply_dim(TokenParser *p) { + push_attr(p, ATTR_DIM, p->dim); + p->dim = true; + mark_dirty(p); +} + +static void apply_italic(TokenParser *p) { + push_attr(p, ATTR_ITALIC, p->italic); + p->italic = true; + mark_dirty(p); +} + +static void apply_underline(TokenParser *p) { + push_attr(p, ATTR_UNDERLINE, p->underline); + p->underline = true; + mark_dirty(p); +} + +static void apply_reverse(TokenParser *p) { + push_attr(p, ATTR_REVERSE, p->reverse); + p->reverse = true; + mark_dirty(p); +} + +static void apply_strikethrough(TokenParser *p) { + push_attr(p, ATTR_STRIKETHROUGH, p->strikethrough); + p->strikethrough = true; + mark_dirty(p); +} + +static void apply_fg_code(TokenParser *p, int code) { + push_attr(p, ATTR_FG, p->fg_color); + p->fg_color = code; + mark_dirty(p); +} + +static void apply_fg_256(TokenParser *p, int n) { + push_attr(p, ATTR_FG, p->fg_color); + p->fg_color = 1000 + n; /* Marker for 256-color */ + mark_dirty(p); +} + +static void apply_bg_code(TokenParser *p, int code) { + push_attr(p, ATTR_BG, p->bg_color); + p->bg_color = code; + mark_dirty(p); +} + +static void apply_bg_256(TokenParser *p, int n) { + push_attr(p, ATTR_BG, p->bg_color); + p->bg_color = 2000 + n; /* Marker for 256-color bg */ + mark_dirty(p); +} + +/* Semantic token applications - deferred emission (just update state) */ +static void apply_token_b(TokenParser *p) { + /* {b} = bold only (same as strong) */ + push_attr(p, ATTR_BOLD, p->bold); + p->bold = true; + mark_dirty(p); +} + +static void apply_token_highlight(TokenParser *p) { + /* {highlight} = bold + yellow */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 33; /* Yellow */ + mark_dirty(p); +} + +static void apply_token_h1(TokenParser *p) { + /* {h1} = bold + orange (256-color 214) */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 1214; /* 256-color marker for 214 */ + mark_dirty(p); +} + +static void apply_token_h2(TokenParser *p) { + /* {h2} = bold + blue */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 34; /* Blue */ + mark_dirty(p); +} + +static void apply_token_h3(TokenParser *p) { + /* {h3} = bold + white */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 37; /* White */ + mark_dirty(p); +} + +static void apply_token_h4(TokenParser *p) { + /* {h4} = bold + white */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 37; + mark_dirty(p); +} + +static void apply_token_h5(TokenParser *p) { + /* {h5} = bold + white */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 37; + mark_dirty(p); +} + +static void apply_token_h6(TokenParser *p) { + /* {h6} = bold + white */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_FG, p->fg_color); + push_composite(p, 2); + p->bold = true; + p->fg_color = 37; + mark_dirty(p); +} + +static void apply_token_strong(TokenParser *p) { + /* {strong} = bold */ + push_attr(p, ATTR_BOLD, p->bold); + p->bold = true; + mark_dirty(p); +} + +static void apply_token_dim_style(TokenParser *p) { + /* {dim} = gray (bright black) */ + push_attr(p, ATTR_FG, p->fg_color); + p->fg_color = 90; /* Bright black */ + mark_dirty(p); +} + +static void apply_token_section(TokenParser *p) { + /* {section} = bold + subtle dark gray background (256-color 237) */ + push_attr(p, ATTR_BOLD, p->bold); + push_attr(p, ATTR_BG, p->bg_color); + push_composite(p, 2); + p->bold = true; + p->bg_color = 2237; /* 256-color marker for 237 */ + mark_dirty(p); +} + +static void apply_token_danger(TokenParser *p) { + /* {danger} = dark red background (256-color 52) */ + push_attr(p, ATTR_BG, p->bg_color); + p->bg_color = 2052; /* 256-color marker for 52 */ + mark_dirty(p); +} + +static void apply_token_text(TokenParser *p) { + /* {text} = full reset */ + reset_all(p); +} + +/* Brighten current foreground color */ +static void apply_bright(TokenParser *p) { + push_attr(p, ATTR_FG, p->fg_color); + /* If current color is 30-37 (standard), convert to 90-97 (bright) */ + if (p->fg_color >= 30 && p->fg_color <= 37) { + p->fg_color = p->fg_color + 60; + } else { + /* Already bright or custom - just set bright white */ + p->fg_color = 97; + } + mark_dirty(p); +} + +/* Parse a number from string, returns -1 on failure */ +static int parse_number(const char *start, const char *end) { + int n = 0; + for (const char *p = start; p < end; p++) { + if (*p < '0' || *p > '9') return -1; + n = n * 10 + (*p - '0'); + } + return n; +} + +%%{ + machine token_parser; + + # Character classes + num_char = [0-9]; + + action mark_start { tok_start = p; } + action mark_end { tok_end = p; } + + # Actions use % to fire AFTER the pattern is complete (leaving action) + + action emit_char { + zstr_push(parser.out, *p); + parser.visual_col++; + } + + action handle_newline { + zstr_push(parser.out, *p); + parser.visual_col = 1; + parser.visual_row++; + } + + action cursor_mark { + parser.cursor_col = parser.visual_col; + parser.cursor_row = parser.visual_row; + parser.has_cursor = true; + } + + action emit_goto_cursor { + if (parser.cursor_row > 0 && parser.cursor_col > 0) { + emit_ansi_num(&parser, "\033[", parser.cursor_row, ";"); + emit_ansi_num(&parser, "", parser.cursor_col, "H"); + } + } + + action pop_style { pop_style(&parser); } + action reset_all { reset_all(&parser); } + action reset_fg { + push_attr(&parser, ATTR_FG, parser.fg_color); + parser.fg_color = 0; + mark_dirty(&parser); + } + action reset_bg { + push_attr(&parser, ATTR_BG, parser.bg_color); + parser.bg_color = 0; + mark_dirty(&parser); + } + + # Semantic tokens + action tok_b { apply_token_b(&parser); } + action tok_highlight { apply_token_highlight(&parser); } + action tok_h1 { apply_token_h1(&parser); } + action tok_h2 { apply_token_h2(&parser); } + action tok_h3 { apply_token_h3(&parser); } + action tok_h4 { apply_token_h4(&parser); } + action tok_h5 { apply_token_h5(&parser); } + action tok_h6 { apply_token_h6(&parser); } + action tok_strong { apply_token_strong(&parser); } + action tok_dim { apply_token_dim_style(&parser); } + action tok_section { apply_token_section(&parser); } + action tok_danger { apply_token_danger(&parser); } + action tok_text { apply_token_text(&parser); } + + # Attribute tokens + action tok_bold { apply_bold(&parser); } + action tok_italic { apply_italic(&parser); } + action tok_underline { apply_underline(&parser); } + action tok_reverse { apply_reverse(&parser); } + action tok_strikethrough { apply_strikethrough(&parser); } + action tok_bright { apply_bright(&parser); } + + # Color tokens - standard foreground + action fg_black { apply_fg_code(&parser, 30); } + action fg_red { apply_fg_code(&parser, 31); } + action fg_green { apply_fg_code(&parser, 32); } + action fg_yellow { apply_fg_code(&parser, 33); } + action fg_blue { apply_fg_code(&parser, 34); } + action fg_magenta { apply_fg_code(&parser, 35); } + action fg_cyan { apply_fg_code(&parser, 36); } + action fg_white { apply_fg_code(&parser, 37); } + action fg_gray { apply_fg_code(&parser, 90); } + + # Color tokens - bright foreground + action fg_bright_black { apply_fg_code(&parser, 90); } + action fg_bright_red { apply_fg_code(&parser, 91); } + action fg_bright_green { apply_fg_code(&parser, 92); } + action fg_bright_yellow { apply_fg_code(&parser, 93); } + action fg_bright_blue { apply_fg_code(&parser, 94); } + action fg_bright_magenta { apply_fg_code(&parser, 95); } + action fg_bright_cyan { apply_fg_code(&parser, 96); } + action fg_bright_white { apply_fg_code(&parser, 97); } + + # Color tokens - standard background + action bg_black { apply_bg_code(&parser, 40); } + action bg_red { apply_bg_code(&parser, 41); } + action bg_green { apply_bg_code(&parser, 42); } + action bg_yellow { apply_bg_code(&parser, 43); } + action bg_blue { apply_bg_code(&parser, 44); } + action bg_magenta { apply_bg_code(&parser, 45); } + action bg_cyan { apply_bg_code(&parser, 46); } + action bg_white { apply_bg_code(&parser, 47); } + action bg_gray { apply_bg_code(&parser, 100); } + + # 256-color handling + action fg_256 { + int n = parse_number(tok_start, tok_end); + if (n >= 0 && n <= 255) { + apply_fg_256(&parser, n); + } + } + + action bg_256 { + int n = parse_number(tok_start, tok_end); + if (n >= 0 && n <= 255) { + apply_bg_256(&parser, n); + } + } + + # Token patterns - use % (leaving action) instead of @ (finishing action) + number = num_char+ >mark_start %mark_end; + + # Reset/pop tokens + # {/} or {/name} all pop one level (name is ignored for flexibility) + tok_pop = "{/" [a-z]* "}" %pop_style; + tok_reset = "{reset}" %reset_all; + tok_reset_fg = "{/fg}" %reset_fg; + tok_reset_bg = "{/bg}" %reset_bg; + + # Semantic tokens + tok_b_open = "{b}" %tok_b; + tok_highlight_open = "{highlight}" %tok_highlight; + tok_h1 = "{h1}" %tok_h1; + tok_h2 = "{h2}" %tok_h2; + tok_h3 = "{h3}" %tok_h3; + tok_h4 = "{h4}" %tok_h4; + tok_h5 = "{h5}" %tok_h5; + tok_h6 = "{h6}" %tok_h6; + tok_strong = "{strong}" %tok_strong; + tok_dim_style = "{dim}" %tok_dim; + tok_section_open = "{section}" %tok_section; + tok_danger_open = "{danger}" %tok_danger; + tok_strike_open = "{strike}" %tok_strikethrough; # Strikethrough text + tok_text = "{text}" %tok_text; + tok_cursor = "{cursor}" %cursor_mark; + + # Attribute tokens + tok_bold = ("{bold}" | "{B}") %tok_bold; + tok_italic = ("{italic}" | "{I}" | "{i}") %tok_italic; + tok_underline = ("{underline}" | "{U}" | "{u}") %tok_underline; + tok_reverse = "{reverse}" %tok_reverse; + tok_strikethrough = "{strikethrough}" %tok_strikethrough; + tok_bright = "{bright}" %tok_bright; + + # Standard foreground colors + tok_fg_black = "{black}" %fg_black; + tok_fg_red = "{red}" %fg_red; + tok_fg_green = "{green}" %fg_green; + tok_fg_yellow = "{yellow}" %fg_yellow; + tok_fg_blue = "{blue}" %fg_blue; + tok_fg_magenta = "{magenta}" %fg_magenta; + tok_fg_cyan = "{cyan}" %fg_cyan; + tok_fg_white = "{white}" %fg_white; + tok_fg_gray = ("{gray}" | "{grey}") %fg_gray; + + # Bright foreground colors + tok_fg_bright_black = "{bright:black}" %fg_bright_black; + tok_fg_bright_red = "{bright:red}" %fg_bright_red; + tok_fg_bright_green = "{bright:green}" %fg_bright_green; + tok_fg_bright_yellow = "{bright:yellow}" %fg_bright_yellow; + tok_fg_bright_blue = "{bright:blue}" %fg_bright_blue; + tok_fg_bright_magenta = "{bright:magenta}" %fg_bright_magenta; + tok_fg_bright_cyan = "{bright:cyan}" %fg_bright_cyan; + tok_fg_bright_white = "{bright:white}" %fg_bright_white; + + # Standard background colors + tok_bg_black = "{bg:black}" %bg_black; + tok_bg_red = "{bg:red}" %bg_red; + tok_bg_green = "{bg:green}" %bg_green; + tok_bg_yellow = "{bg:yellow}" %bg_yellow; + tok_bg_blue = "{bg:blue}" %bg_blue; + tok_bg_magenta = "{bg:magenta}" %bg_magenta; + tok_bg_cyan = "{bg:cyan}" %bg_cyan; + tok_bg_white = "{bg:white}" %bg_white; + tok_bg_gray = ("{bg:gray}" | "{bg:grey}") %bg_gray; + + # 256-color tokens + tok_fg_256 = "{fg:" number "}" %fg_256; + tok_bg_256 = "{bg:" number "}" %bg_256; + + # Control sequence tokens (for convenience in TUI code) + action emit_clear_line { emit_ansi(&parser, "\033[K"); } + action emit_clear_screen { emit_ansi(&parser, "\033[J"); } + action emit_home { emit_ansi(&parser, "\033[H"); } + action emit_hide_cursor { emit_ansi(&parser, "\033[?25l"); } + action emit_show_cursor { emit_ansi(&parser, "\033[?25h"); } + + # Cursor positioning: {goto:row,col} + action mark_row_start { goto_row_start = p; } + action mark_row_end { goto_row_end = p; } + action mark_col_start { goto_col_start = p; } + action mark_col_end { goto_col_end = p; } + action emit_goto { + int row = parse_number(goto_row_start, goto_row_end); + int col = parse_number(goto_col_start, goto_col_end); + if (row >= 0 && col >= 0) { + emit_ansi_num(&parser, "\033[", row, ";"); + emit_ansi_num(&parser, "", col, "H"); + } + } + + tok_clear_line = "{clr}" %emit_clear_line; + tok_clear_screen = "{cls}" %emit_clear_screen; + tok_home = "{home}" %emit_home; + tok_hide_cursor = "{hide}" %emit_hide_cursor; + tok_show_cursor = "{show}" %emit_show_cursor; + tok_goto = "{goto:" (num_char+ >mark_row_start %mark_row_end) "," (num_char+ >mark_col_start %mark_col_end) "}" %emit_goto; + tok_goto_cursor = "{goto_cursor}" %emit_goto_cursor; + + # All tokens combined + token = ( + tok_pop | + tok_reset | + tok_reset_fg | + tok_reset_bg | + tok_b_open | + tok_highlight_open | + tok_h1 | + tok_h2 | + tok_h3 | + tok_h4 | + tok_h5 | + tok_h6 | + tok_strong | + tok_dim_style | + tok_section_open | + tok_danger_open | + tok_strike_open | + tok_text | + tok_cursor | + tok_bold | + tok_italic | + tok_underline | + tok_reverse | + tok_strikethrough | + tok_bright | + tok_fg_black | + tok_fg_red | + tok_fg_green | + tok_fg_yellow | + tok_fg_blue | + tok_fg_magenta | + tok_fg_cyan | + tok_fg_white | + tok_fg_gray | + tok_fg_bright_black | + tok_fg_bright_red | + tok_fg_bright_green | + tok_fg_bright_yellow | + tok_fg_bright_blue | + tok_fg_bright_magenta | + tok_fg_bright_cyan | + tok_fg_bright_white | + tok_bg_black | + tok_bg_red | + tok_bg_green | + tok_bg_yellow | + tok_bg_blue | + tok_bg_magenta | + tok_bg_cyan | + tok_bg_white | + tok_bg_gray | + tok_fg_256 | + tok_bg_256 | + tok_clear_line | + tok_clear_screen | + tok_home | + tok_hide_cursor | + tok_show_cursor | + tok_goto | + tok_goto_cursor + ); + + # Helper action to emit the matched token text + action emit_token { + for (const char *c = ts; c < te; c++) { + zstr_push(parser.out, *c); + if (*c != '\033' && *c != '[' && (*c < '0' || *c > '9') && *c != ';') { + if (*c != '\n') { + parser.visual_col++; + } else { + parser.visual_col = 1; + } + } + } + } + + action emit_regular_char { + sync_styles(&parser); /* Emit any pending style changes */ + zstr_push(parser.out, *ts); + parser.visual_col++; + } + + action emit_newline { + /* Auto-reset all active styles before newline */ + reset_line_styles(&parser); + zstr_push(parser.out, *ts); + parser.visual_col = 1; + } + + action emit_ansi_passthrough { + for (const char *c = ts; c < te; c++) { + zstr_push(parser.out, *c); + } + } + + # ANSI escape sequence passthrough (don't count as visible) + ansi_seq = 0x1B '[' (any - alpha)* alpha; + + # Regular character (not start of token, not escape, not newline) + regular_char = (any - '{' - 0x1B - '\n'); + + # Newline handling + newline = '\n'; + + # Unrecognized { - just emit it + open_brace = '{'; + + # Main scanner using |* for proper longest-match scanning + # Use => for scanner actions (fire on token acceptance) + main := |* + token => {}; + ansi_seq => emit_ansi_passthrough; + newline => emit_newline; + regular_char => emit_regular_char; + open_brace => emit_regular_char; + *|; + + write data; +}%% + +/* Global flags */ +bool zstr_disable_token_expansion = false; +bool zstr_no_colors = false; + +TokenExpansion expand_tokens_with_cursor(const char *text) { + TokenExpansion result = {0}; + result.expanded = zstr_init(); + result.cursor_col = -1; + result.cursor_row = -1; + result.has_cursor = false; + result.final_col = 1; + result.final_row = 1; + + if (!text || !*text) { + return result; + } + + /* If expansion is disabled, just copy */ + if (zstr_disable_token_expansion) { + zstr_cat(&result.expanded, text); + return result; + } + + /* Pre-reserve buffer space to avoid reallocations. + * Estimate: input length + overhead for ANSI codes (~50% extra) */ + size_t input_len = strlen(text); + zstr_reserve(&result.expanded, input_len + input_len / 2 + 64); + + /* Initialize parser state */ + TokenParser parser = {0}; + parser.out = &result.expanded; + parser.cursor_col = -1; + parser.cursor_row = -1; + parser.visual_col = 1; + parser.visual_row = 1; + parser.no_colors = zstr_no_colors; + + /* Ragel variables */ + int cs; + int act; + const char *p = text; + const char *pe = text + strlen(text); + const char *eof = pe; + const char *ts = NULL; /* Token start */ + const char *te = NULL; /* Token end */ + const char *tok_start = NULL; + const char *tok_end = NULL; + const char *goto_row_start = NULL; + const char *goto_row_end = NULL; + const char *goto_col_start = NULL; + const char *goto_col_end = NULL; + + (void)tok_start; + (void)tok_end; + (void)goto_row_start; + (void)goto_row_end; + (void)goto_col_start; + (void)goto_col_end; + (void)eof; + (void)ts; + (void)te; + (void)act; + (void)token_parser_en_main; + + %% write init; + %% write exec; + + result.cursor_col = parser.cursor_col; + result.cursor_row = parser.cursor_row; + result.has_cursor = parser.has_cursor; + result.final_col = parser.visual_col; + result.final_row = parser.visual_row; + return result; +} + +zstr expand_tokens(const char *text) { + TokenExpansion result = expand_tokens_with_cursor(text); + return result.expanded; +} + +void zstr_expand_to(FILE *stream, const char *text) { + if (!text || !*text) return; + + Z_CLEANUP(zstr_free) zstr expanded = expand_tokens(text); + fwrite(zstr_cstr(&expanded), 1, zstr_len(&expanded), stream); +} + +void token_expansion_render(FILE *stream, TokenExpansion *te) { + if (!te) return; + + /* Write the expanded content */ + fwrite(zstr_cstr(&te->expanded), 1, zstr_len(&te->expanded), stream); + + /* If cursor was marked, clear to end of line and position cursor */ + if (te->has_cursor) { + /* Clear from current position to end of line (gives cursor room) */ + if (!zstr_no_colors) { + fprintf(stream, "\033[K"); + } + + /* Position cursor at marked location and show it */ + if (!zstr_no_colors && te->cursor_row > 0 && te->cursor_col > 0) { + fprintf(stream, "\033[%d;%dH\033[?25h", te->cursor_row, te->cursor_col); + } + } +} diff --git a/src/tokens_test.c b/src/tokens_test.c new file mode 100644 index 0000000..b8213fb --- /dev/null +++ b/src/tokens_test.c @@ -0,0 +1,475 @@ +/* + * Token system unit tests + * Uses acutest: https://github.com/mity/acutest + */ + +#include "acutest.h" +#include "tokens.h" +#include + +/* Helper to check if a string contains a substring */ +static int contains(const char *haystack, const char *needle) { + return strstr(haystack, needle) != NULL; +} + +/* Helper to count occurrences of a substring */ +static int count_occurrences(const char *haystack, const char *needle) { + int count = 0; + const char *p = haystack; + size_t needle_len = strlen(needle); + while ((p = strstr(p, needle)) != NULL) { + count++; + p += needle_len; + } + return count; +} + +/* + * Test: {b} token should emit just bold, not bold+yellow + */ +void test_b_is_just_bold(void) { + zstr result = expand_tokens("{b}test{/}"); + const char *s = zstr_cstr(&result); + + /* Should contain bold code */ + TEST_CHECK(contains(s, "\033[1m") || contains(s, "\033[0;1m")); + /* Should NOT contain yellow code 33 */ + TEST_CHECK(!contains(s, ";33m") && !contains(s, "[33m")); + TEST_CHECK(contains(s, "test")); + + zstr_free(&result); +} + +/* + * Test: {highlight} token should emit bold+yellow + */ +void test_highlight_is_bold_yellow(void) { + zstr result = expand_tokens("{highlight}test{/}"); + const char *s = zstr_cstr(&result); + + /* Should contain both bold and yellow */ + TEST_CHECK(contains(s, "1") && contains(s, "33")); + TEST_CHECK(contains(s, "test")); + + zstr_free(&result); +} + +/* + * Test: Redundant tokens should not emit multiple codes + * "{dim}{dim}{dim}b" should emit only one \033[90m + */ +void test_redundant_tokens_no_duplicate_codes(void) { + zstr result = expand_tokens("{dim}{dim}{dim}b"); + const char *s = zstr_cstr(&result); + + /* Should contain exactly one dim code (90) */ + int count = count_occurrences(s, "\033[90m"); + TEST_CHECK_(count == 1, "Expected 1 dim code, got %d", count); + TEST_CHECK(contains(s, "b")); + + zstr_free(&result); +} + +/* + * Test: Deferred emission - codes only emit when there's a character + * "{b}{/}x" should emit just "x" with no ANSI codes + */ +void test_deferred_emission_no_unused_codes(void) { + zstr result = expand_tokens("{b}{/}x"); + const char *s = zstr_cstr(&result); + + /* Should just be "x" with no escape codes */ + TEST_CHECK_(!contains(s, "\033"), "Expected no ANSI codes, got: %s", s); + TEST_CHECK(strcmp(s, "x") == 0); + + zstr_free(&result); +} + +/* + * Test: Auto-reset at newlines + * "{b}bold text\nnormal" should have reset before newline + */ +void test_auto_reset_at_newline(void) { + zstr result = expand_tokens("{b}bold text\nnormal"); + const char *s = zstr_cstr(&result); + + /* Should contain reset code before the newline */ + const char *newline_pos = strchr(s, '\n'); + TEST_CHECK(newline_pos != NULL); + + /* The reset (0m) should appear before the newline */ + const char *reset_pos = strstr(s, "\033[0m"); + TEST_CHECK(reset_pos != NULL); + TEST_CHECK(reset_pos < newline_pos); + + /* "normal" should NOT have bold applied (comes after newline) */ + const char *normal_start = newline_pos + 1; + /* Check that there's no bold code right before "normal" */ + TEST_CHECK(strncmp(normal_start, "normal", 6) == 0); + + zstr_free(&result); +} + +/* + * Test: Stack-based nesting works correctly + */ +void test_stack_nesting(void) { + zstr result = expand_tokens("{bold}{red}both{/}just bold{/}normal"); + const char *s = zstr_cstr(&result); + + TEST_CHECK(contains(s, "both")); + TEST_CHECK(contains(s, "just bold")); + TEST_CHECK(contains(s, "normal")); + + zstr_free(&result); +} + +/* + * Test: {danger} token works (renamed from {strike}) + */ +void test_danger_token(void) { + zstr result = expand_tokens("{danger}warning{/}"); + const char *s = zstr_cstr(&result); + + /* Should contain background color 48;5;52 (dark red bg) */ + TEST_CHECK(contains(s, "48;5;52") || contains(s, "48;5")); + TEST_CHECK(contains(s, "warning")); + + zstr_free(&result); +} + +/* + * Test: {strike} is strikethrough text + */ +void test_strike_is_strikethrough(void) { + zstr result = expand_tokens("{strike}crossed{/}"); + const char *s = zstr_cstr(&result); + + /* Should contain strikethrough code (9) */ + TEST_CHECK(contains(s, "9") || contains(s, ";9")); + TEST_CHECK(contains(s, "crossed")); + + zstr_free(&result); +} + +/* + * Test: {/name} works as generic pop (e.g., {/highlight}) + */ +void test_generic_pop(void) { + zstr result1 = expand_tokens("{highlight}a{/highlight}b"); + zstr result2 = expand_tokens("{highlight}a{/}b"); + + /* Both should produce same output */ + TEST_CHECK(strcmp(zstr_cstr(&result1), zstr_cstr(&result2)) == 0); + + zstr_free(&result1); + zstr_free(&result2); +} + +/* + * Test: Heading tokens work + */ +void test_heading_tokens(void) { + zstr h1 = expand_tokens("{h1}H1{/}"); + zstr h2 = expand_tokens("{h2}H2{/}"); + zstr h3 = expand_tokens("{h3}H3{/}"); + + /* All should contain bold (1) */ + TEST_CHECK(contains(zstr_cstr(&h1), "H1")); + TEST_CHECK(contains(zstr_cstr(&h2), "H2")); + TEST_CHECK(contains(zstr_cstr(&h3), "H3")); + + /* h1 should have 256-color orange (38;5;214) */ + TEST_CHECK(contains(zstr_cstr(&h1), "38;5;214")); + + /* h2 should have blue (34) */ + TEST_CHECK(contains(zstr_cstr(&h2), "34")); + + zstr_free(&h1); + zstr_free(&h2); + zstr_free(&h3); +} + +/* + * Test: 256-color foreground + */ +void test_256_color_fg(void) { + zstr result = expand_tokens("{fg:214}orange{/}"); + const char *s = zstr_cstr(&result); + + TEST_CHECK(contains(s, "38;5;214")); + TEST_CHECK(contains(s, "orange")); + + zstr_free(&result); +} + +/* + * Test: 256-color background + */ +void test_256_color_bg(void) { + zstr result = expand_tokens("{bg:52}darkred{/}"); + const char *s = zstr_cstr(&result); + + TEST_CHECK(contains(s, "48;5;52")); + TEST_CHECK(contains(s, "darkred")); + + zstr_free(&result); +} + +/* + * Test: Cursor position tracking + */ +void test_cursor_tracking(void) { + TokenExpansion te = expand_tokens_with_cursor("Hello {cursor}World"); + + /* Cursor should be at column 7 (after "Hello "), row 1 */ + TEST_CHECK_(te.cursor_col == 7, "Expected cursor_col at 7, got %d", te.cursor_col); + TEST_CHECK_(te.cursor_row == 1, "Expected cursor_row at 1, got %d", te.cursor_row); + TEST_CHECK_(te.has_cursor == true, "Expected has_cursor to be true"); + TEST_CHECK(contains(zstr_cstr(&te.expanded), "Hello")); + TEST_CHECK(contains(zstr_cstr(&te.expanded), "World")); + + token_expansion_free(&te); +} + +/* + * Test: Control tokens ({clr}, {home}, etc.) + */ +void test_control_tokens(void) { + zstr clr = expand_tokens("{clr}"); + zstr home = expand_tokens("{home}"); + zstr hide = expand_tokens("{hide}"); + zstr show = expand_tokens("{show}"); + + TEST_CHECK(strcmp(zstr_cstr(&clr), "\033[K") == 0); + TEST_CHECK(strcmp(zstr_cstr(&home), "\033[H") == 0); + TEST_CHECK(strcmp(zstr_cstr(&hide), "\033[?25l") == 0); + TEST_CHECK(strcmp(zstr_cstr(&show), "\033[?25h") == 0); + + zstr_free(&clr); + zstr_free(&home); + zstr_free(&hide); + zstr_free(&show); +} + +/* + * Test: zstr_no_colors flag disables output + */ +void test_no_colors_flag(void) { + zstr_no_colors = true; + zstr result = expand_tokens("{b}text{/}"); + + /* Should just be "text" with no ANSI codes */ + TEST_CHECK(strcmp(zstr_cstr(&result), "text") == 0); + + zstr_free(&result); + zstr_no_colors = false; +} + +/* + * Test: zstr_disable_token_expansion flag + */ +void test_disable_expansion_flag(void) { + zstr_disable_token_expansion = true; + zstr result = expand_tokens("{b}text{/}"); + + /* Should pass through unchanged */ + TEST_CHECK(strcmp(zstr_cstr(&result), "{b}text{/}") == 0); + + zstr_free(&result); + zstr_disable_token_expansion = false; +} + +/* + * Test: {strong} is same as {b} + */ +void test_strong_same_as_b(void) { + zstr b_result = expand_tokens("{b}x{/}"); + zstr strong_result = expand_tokens("{strong}x{/}"); + + /* Both should produce same output */ + TEST_CHECK(strcmp(zstr_cstr(&b_result), zstr_cstr(&strong_result)) == 0); + + zstr_free(&b_result); + zstr_free(&strong_result); +} + +/* + * Test: {dim} token + */ +void test_dim_token(void) { + zstr result = expand_tokens("{dim}dimmed{/}"); + const char *s = zstr_cstr(&result); + + /* Should contain code 90 (bright black/gray) */ + TEST_CHECK(contains(s, "90")); + TEST_CHECK(contains(s, "dimmed")); + + zstr_free(&result); +} + +/* + * Test: ANSI passthrough (existing escape sequences are preserved) + */ +void test_ansi_passthrough(void) { + zstr result = expand_tokens("hello\033[31mred\033[0mworld"); + const char *s = zstr_cstr(&result); + + /* Original ANSI codes should be preserved */ + TEST_CHECK(contains(s, "\033[31m")); + TEST_CHECK(contains(s, "\033[0m")); + TEST_CHECK(contains(s, "hello")); + TEST_CHECK(contains(s, "red")); + TEST_CHECK(contains(s, "world")); + + zstr_free(&result); +} + +/* + * Test: Empty input + */ +void test_empty_input(void) { + zstr result = expand_tokens(""); + TEST_CHECK(zstr_len(&result) == 0); + zstr_free(&result); + + zstr result2 = expand_tokens(NULL); + TEST_CHECK(zstr_len(&result2) == 0); + zstr_free(&result2); +} + +/* + * Test: Unrecognized tokens pass through + */ +void test_unrecognized_tokens(void) { + zstr result = expand_tokens("{unknown}text"); + const char *s = zstr_cstr(&result); + + /* Unrecognized {unknown} should be passed through as-is */ + TEST_CHECK(contains(s, "{unknown}")); + TEST_CHECK(contains(s, "text")); + + zstr_free(&result); +} + +/* + * Test: Tokens before newline don't output control sequences + * "{bold}\n" should just output "\n" with no ANSI codes + */ +void test_tokens_before_newline_no_codes(void) { + zstr result = expand_tokens("{bold}\n"); + const char *s = zstr_cstr(&result); + + /* Should just be a newline with no escape codes */ + TEST_CHECK_(strcmp(s, "\n") == 0, "Expected just newline, got: %s", s); + + zstr_free(&result); + + /* Multiple tokens before newline */ + zstr result2 = expand_tokens("{red}{blue}{bold}\n"); + const char *s2 = zstr_cstr(&result2); + TEST_CHECK_(strcmp(s2, "\n") == 0, "Expected just newline, got: %s", s2); + + zstr_free(&result2); +} + +/* + * Test: Pop back to same state doesn't emit redundant codes + * {green}a{blue}{/}b should emit green once, then output "ab" + * because after popping blue, we're back to green (already emitted) + */ +void test_pop_to_same_state_no_redundant_codes(void) { + zstr result = expand_tokens("{green}a{blue}{/}b"); + const char *s = zstr_cstr(&result); + + /* Should contain green code exactly once */ + int green_count = count_occurrences(s, "\033[32m"); + TEST_CHECK_(green_count == 1, "Expected 1 green code, got %d", green_count); + + /* Should NOT contain blue code (no character printed while blue was active) */ + TEST_CHECK_(!contains(s, "\033[34m"), "Should not contain blue code"); + + /* Should contain both a and b */ + TEST_CHECK(contains(s, "a")); + TEST_CHECK(contains(s, "b")); + + zstr_free(&result); +} + +/* + * Test: Multiple pushes with deferred emission + * Tokens before a character don't emit until the character + * {blue}{green}{blue}{red}{green}a should emit only one code (green) right before 'a' + */ +void test_multiple_pushes_deferred(void) { + zstr result = expand_tokens("{blue}{green}{blue}{red}{green}a"); + const char *s = zstr_cstr(&result); + + /* Should contain exactly one escape sequence (green) followed by 'a' */ + /* The format should be \033[32ma or similar */ + TEST_CHECK(contains(s, "32")); /* green color code */ + TEST_CHECK(contains(s, "a")); + + /* Count total escape sequences - should be exactly 1 */ + int esc_count = count_occurrences(s, "\033["); + TEST_CHECK_(esc_count == 1, "Expected 1 escape sequence, got %d", esc_count); + + zstr_free(&result); +} + +/* + * Test: Complex push/pop sequence + * After multiple pops, only emit if state actually changes from last emitted + */ +void test_complex_push_pop_sequence(void) { + /* {green}a{red}b{/}c - after pop, back to green, same as before red was pushed */ + zstr result = expand_tokens("{green}a{red}b{/}c"); + const char *s = zstr_cstr(&result); + + /* Should have green, red, then green again (because we popped back) */ + TEST_CHECK(contains(s, "a")); + TEST_CHECK(contains(s, "b")); + TEST_CHECK(contains(s, "c")); + + /* Count green codes - should be 2 (initial green for 'a', restored green for 'c') */ + int green_count = count_occurrences(s, "\033[32m"); + TEST_CHECK_(green_count == 2, "Expected 2 green codes, got %d", green_count); + + /* Count red codes - should be 1 (for 'b') */ + int red_count = count_occurrences(s, "\033[31m"); + TEST_CHECK_(red_count == 1, "Expected 1 red code, got %d", red_count); + + zstr_free(&result); +} + +/* + * Test list + */ +TEST_LIST = { + { "b_is_just_bold", test_b_is_just_bold }, + { "highlight_is_bold_yellow", test_highlight_is_bold_yellow }, + { "redundant_tokens_no_duplicate_codes", test_redundant_tokens_no_duplicate_codes }, + { "deferred_emission_no_unused_codes", test_deferred_emission_no_unused_codes }, + { "auto_reset_at_newline", test_auto_reset_at_newline }, + { "stack_nesting", test_stack_nesting }, + { "danger_token", test_danger_token }, + { "strike_is_strikethrough", test_strike_is_strikethrough }, + { "generic_pop", test_generic_pop }, + { "heading_tokens", test_heading_tokens }, + { "256_color_fg", test_256_color_fg }, + { "256_color_bg", test_256_color_bg }, + { "cursor_tracking", test_cursor_tracking }, + { "control_tokens", test_control_tokens }, + { "no_colors_flag", test_no_colors_flag }, + { "disable_expansion_flag", test_disable_expansion_flag }, + { "strong_same_as_b", test_strong_same_as_b }, + { "dim_token", test_dim_token }, + { "ansi_passthrough", test_ansi_passthrough }, + { "empty_input", test_empty_input }, + { "unrecognized_tokens", test_unrecognized_tokens }, + { "tokens_before_newline_no_codes", test_tokens_before_newline_no_codes }, + { "pop_to_same_state_no_redundant_codes", test_pop_to_same_state_no_redundant_codes }, + { "multiple_pushes_deferred", test_multiple_pushes_deferred }, + { "complex_push_pop_sequence", test_complex_push_pop_sequence }, + { NULL, NULL } +}; diff --git a/src/tui.c b/src/tui.c index c09a9f8..a7be5f1 100644 --- a/src/tui.c +++ b/src/tui.c @@ -352,8 +352,7 @@ static bool render_delete_confirmation(const char *base_path, Mode *mode) { bool is_test = (mode && mode->inject_keys); while (1) { - WRITE(STDERR_FILENO, "\x1b[?25l", 6); // Hide cursor - WRITE(STDERR_FILENO, "\x1b[H", 3); // Home + zstr_expand_to(stderr, "{hide}{home}"); // Title Z_CLEANUP(zstr_free) zstr title = zstr_from("{b}Delete "); @@ -362,10 +361,8 @@ static bool render_delete_confirmation(const char *base_path, Mode *mode) { zstr_cat(&title, count_str); zstr_cat(&title, " director"); zstr_cat(&title, marked_items.length == 1 ? "y" : "ies"); - zstr_cat(&title, "?{/b}\x1b[K\n\x1b[K\n"); - - Z_CLEANUP(zstr_free) zstr title_exp = zstr_expand_tokens(zstr_cstr(&title)); - WRITE(STDERR_FILENO, zstr_cstr(&title_exp), zstr_len(&title_exp)); + zstr_cat(&title, "?{/b}{clr}\n{clr}\n"); + zstr_expand_to(stderr, zstr_cstr(&title)); // List items (max 10) int max_show = 10; @@ -373,24 +370,18 @@ static bool render_delete_confirmation(const char *base_path, Mode *mode) { for (int i = 0; i < max_show; i++) { Z_CLEANUP(zstr_free) zstr item = zstr_from(" {dim}-{reset} "); zstr_cat(&item, zstr_cstr(&marked_items.data[i]->name)); - zstr_cat(&item, "\x1b[K\n"); - Z_CLEANUP(zstr_free) zstr item_exp = zstr_expand_tokens(zstr_cstr(&item)); - WRITE(STDERR_FILENO, zstr_cstr(&item_exp), zstr_len(&item_exp)); + zstr_cat(&item, "{clr}\n"); + zstr_expand_to(stderr, zstr_cstr(&item)); } if ((int)marked_items.length > max_show) { - char more[64]; - snprintf(more, sizeof(more), " {dim}...and %zu more{reset}\x1b[K\n", + char more[80]; + snprintf(more, sizeof(more), " {dim}...and %zu more{reset}{clr}\n", marked_items.length - max_show); - Z_CLEANUP(zstr_free) zstr more_exp = zstr_expand_tokens(more); - WRITE(STDERR_FILENO, zstr_cstr(&more_exp), zstr_len(&more_exp)); + zstr_expand_to(stderr, more); } - // Prompt - track line for cursor positioning - // Line 1: title, Line 2: blank, Lines 3..3+items-1: items, then blank, then prompt - // So prompt is at line: 1 (title) + 1 (blank) + items + 1 (blank before prompt) + 1 = 4 + items - int prompt_line = 4 + max_show + ((int)marked_items.length > max_show ? 1 : 0); - - Z_CLEANUP(zstr_free) zstr prompt = zstr_from("\x1b[K\n{dim}Type {/fg}{b}YES{/b}{dim} to confirm:{reset} "); + // Prompt + Z_CLEANUP(zstr_free) zstr prompt = zstr_from("{clr}\n{dim}Type {/}{highlight}YES{/}{dim} to confirm:{reset} "); const char *confirm_cstr = zstr_cstr(&confirm_input); int confirm_len = (int)zstr_len(&confirm_input); @@ -404,21 +395,11 @@ static bool render_delete_confirmation(const char *base_path, Mode *mode) { zstr_cat_len(&prompt, confirm_cstr, cursor); zstr_cat(&prompt, "{cursor}"); zstr_cat_len(&prompt, confirm_cstr + cursor, confirm_len - cursor); + zstr_cat(&prompt, "\n{cls}"); - zstr_cat(&prompt, "\x1b[K"); - - TokenExpansion prompt_exp = zstr_expand_tokens_with_cursor(zstr_cstr(&prompt)); - int prompt_col = prompt_exp.cursor_pos; - WRITE(STDERR_FILENO, zstr_cstr(&prompt_exp.expanded), zstr_len(&prompt_exp.expanded)); - zstr_free(&prompt_exp.expanded); - - WRITE(STDERR_FILENO, "\n\x1b[J", 4); // Newline then clear rest - - // Position cursor on prompt line after input - char cursor_pos[32]; - snprintf(cursor_pos, sizeof(cursor_pos), "\x1b[%d;%dH", prompt_line, prompt_col); - WRITE(STDERR_FILENO, cursor_pos, strlen(cursor_pos)); - WRITE(STDERR_FILENO, "\x1b[?25h", 6); // Show cursor + TokenExpansion te = zstr_expand_tokens_with_cursor(zstr_cstr(&prompt)); + token_expansion_render(stderr, &te); + token_expansion_free(&te); // Read key int c; @@ -473,8 +454,7 @@ static void render(const char *base_path) { int rows, cols; get_window_size(&rows, &cols); - WRITE(STDERR_FILENO, "\x1b[?25l", 6); // Hide cursor - WRITE(STDERR_FILENO, "\x1b[H", 3); // Home + zstr_expand_to(stderr, "{hide}{home}"); // Build separator line dynamically (handles any terminal width) Z_CLEANUP(zstr_free) zstr sep_line = zstr_init(); @@ -482,16 +462,13 @@ static void render(const char *base_path) { zstr_cat(&sep_line, "─"); // Header - // Use AUTO_ZSTR for temp strings { Z_CLEANUP(zstr_free) zstr header_fmt = - zstr_from("{h1}πŸ“ Try Directory Selection{reset}\x1b[K\n{dim}"); + zstr_from("{h1}πŸ“ Try Directory Selection{reset}{clr}\n{dim}"); zstr_cat(&header_fmt, zstr_cstr(&sep_line)); - zstr_cat(&header_fmt, "{reset}\x1b[K\n"); - - Z_CLEANUP(zstr_free) zstr header = zstr_expand_tokens(zstr_cstr(&header_fmt)); - WRITE(STDERR_FILENO, zstr_cstr(&header), zstr_len(&header)); + zstr_cat(&header_fmt, "{reset}{clr}\n"); + zstr_expand_to(stderr, zstr_cstr(&header_fmt)); } // Search bar - track cursor position @@ -522,14 +499,14 @@ static void render(const char *base_path) { zstr_push(&search_fmt, filter_cstr[i]); } - zstr_cat(&search_fmt, "\x1b[K\n{dim}"); + zstr_cat(&search_fmt, "{clr}\n{dim}"); zstr_cat(&search_fmt, zstr_cstr(&sep_line)); - zstr_cat(&search_fmt, "{reset}\x1b[K\n"); + zstr_cat(&search_fmt, "{reset}{clr}\n"); TokenExpansion search_exp = zstr_expand_tokens_with_cursor(zstr_cstr(&search_fmt)); - search_cursor_col = search_exp.cursor_pos; - WRITE(STDERR_FILENO, zstr_cstr(&search_exp.expanded), zstr_len(&search_exp.expanded)); - zstr_free(&search_exp.expanded); + search_cursor_col = search_exp.cursor_col; + fwrite(zstr_cstr(&search_exp.expanded), 1, zstr_len(&search_exp.expanded), stderr); + token_expansion_free(&search_exp); } // List @@ -603,19 +580,18 @@ static void render(const char *base_path) { bool is_marked = entry->marked_for_delete; if (is_selected) { if (is_marked) { - zstr_cat(&line, "{b}β†’ {/b}πŸ—‘οΈ {strike}{section}"); + zstr_cat(&line, "{section}{highlight}β†’ {/}πŸ—‘οΈ {danger}"); zstr_cat(&line, zstr_cstr(&display_name)); - zstr_cat(&line, "{/section}{/strike}"); + zstr_cat(&line, "{/}"); } else { - zstr_cat(&line, "{b}β†’ {/b}πŸ“ {section}"); + zstr_cat(&line, "{section}{highlight}β†’ {/}πŸ“ "); zstr_cat(&line, zstr_cstr(&display_name)); - zstr_cat(&line, "{/section}"); } } else { if (is_marked) { - zstr_cat(&line, " πŸ—‘οΈ {strike}"); + zstr_cat(&line, " πŸ—‘οΈ {danger}"); zstr_cat(&line, zstr_cstr(&display_name)); - zstr_cat(&line, "{/strike}"); + zstr_cat(&line, "{/}"); } else { zstr_cat(&line, " πŸ“ "); zstr_cat(&line, zstr_cstr(&display_name)); @@ -658,14 +634,12 @@ static void render(const char *base_path) { } } - zstr_cat(&line, "\x1b[K\n"); - - Z_CLEANUP(zstr_free) zstr exp = zstr_expand_tokens(zstr_cstr(&line)); - WRITE(STDERR_FILENO, zstr_cstr(&exp), zstr_len(&exp)); + zstr_cat(&line, "{clr}\n"); + zstr_expand_to(stderr, zstr_cstr(&line)); } else if (idx == (int)filtered_ptrs.length && zstr_len(&filter_buffer) > 0) { // Add separator line before "Create new" - WRITE(STDERR_FILENO, "\x1b[K\n", 4); + zstr_expand_to(stderr, "{clr}\n"); i++; // Skip next iteration since we used a line for separator // Generate preview of what the directory name will be @@ -689,57 +663,52 @@ static void render(const char *base_path) { if (idx == selected_index) { Z_CLEANUP(zstr_free) - zstr line = zstr_from("{b}β†’ {/b}πŸ“‚ Create new: {dim}"); + zstr line = zstr_from("{section}{highlight}β†’ {/}πŸ“‚ Create new: {dim}"); zstr_cat(&line, zstr_cstr(&preview)); - zstr_cat(&line, "{reset}\x1b[K\n"); - - Z_CLEANUP(zstr_free) zstr exp = zstr_expand_tokens(zstr_cstr(&line)); - WRITE(STDERR_FILENO, zstr_cstr(&exp), zstr_len(&exp)); + zstr_cat(&line, "{/}{clr}\n"); + zstr_expand_to(stderr, zstr_cstr(&line)); } else { Z_CLEANUP(zstr_free) zstr line = zstr_from(" πŸ“‚ Create new: {dim}"); zstr_cat(&line, zstr_cstr(&preview)); - zstr_cat(&line, "{reset}\x1b[K\n"); - - Z_CLEANUP(zstr_free) zstr exp = zstr_expand_tokens(zstr_cstr(&line)); - WRITE(STDERR_FILENO, zstr_cstr(&exp), zstr_len(&exp)); + zstr_cat(&line, "{reset}{clr}\n"); + zstr_expand_to(stderr, zstr_cstr(&line)); } } else { - WRITE(STDERR_FILENO, "\x1b[K\n", 4); + zstr_expand_to(stderr, "{clr}\n"); } } - WRITE(STDERR_FILENO, "\x1b[J", 3); // Clear rest + zstr_expand_to(stderr, "{cls}"); // Footer { Z_CLEANUP(zstr_free) zstr footer_fmt = zstr_from("{dim}"); zstr_cat(&footer_fmt, zstr_cstr(&sep_line)); - zstr_cat(&footer_fmt, "{reset}\x1b[K\n"); + zstr_cat(&footer_fmt, "{reset}{clr}\n"); if (marked_count > 0) { // Delete mode footer char count_str[32]; snprintf(count_str, sizeof(count_str), "%d", marked_count); - zstr_cat(&footer_fmt, "{b}DELETE MODE{/b} | "); + zstr_cat(&footer_fmt, "{highlight}DELETE MODE{/} | "); zstr_cat(&footer_fmt, count_str); - zstr_cat(&footer_fmt, " marked | {dim}Ctrl-D: Toggle Enter: Confirm Esc: Cancel{reset}\x1b[K\n"); + zstr_cat(&footer_fmt, " marked | {dim}Ctrl-D: Toggle Enter: Confirm Esc: Cancel{reset}{clr}\n"); } else { // Normal footer - zstr_cat(&footer_fmt, "{dim}↑/↓: Navigate Enter: Select Ctrl-D: Delete Esc: Cancel{reset}\x1b[K\n"); + zstr_cat(&footer_fmt, "{dim}↑/↓: Navigate Enter: Select Ctrl-D: Delete Esc: Cancel{reset}{clr}\n"); } - Z_CLEANUP(zstr_free) zstr footer = zstr_expand_tokens(zstr_cstr(&footer_fmt)); - WRITE(STDERR_FILENO, zstr_cstr(&footer), zstr_len(&footer)); - } - - // Position cursor in search field and show it - if (search_cursor_col >= 0) { - char cursor_pos[32]; - snprintf(cursor_pos, sizeof(cursor_pos), "\x1b[%d;%dH", search_line, search_cursor_col); - WRITE(STDERR_FILENO, cursor_pos, strlen(cursor_pos)); + // Position cursor in search field and show it + if (search_cursor_col >= 0) { + char goto_cmd[32]; + snprintf(goto_cmd, sizeof(goto_cmd), "{goto:%d,%d}{show}", search_line, search_cursor_col); + zstr_cat(&footer_fmt, goto_cmd); + } else { + zstr_cat(&footer_fmt, "{show}"); + } + zstr_expand_to(stderr, zstr_cstr(&footer_fmt)); } - WRITE(STDERR_FILENO, "\x1b[?25h", 6); // Show cursor (void)base_path; } @@ -920,7 +889,7 @@ SelectionResult run_selector(const char *base_path, // Reset terminal state disable_raw_mode(); // Reset all attributes - fprintf(stderr, "\x1b[0m"); + zstr_expand_to(stderr, "{reset}"); fflush(stderr); } diff --git a/src/utils.c b/src/utils.c index 115890e..cb8b96a 100644 --- a/src/utils.c +++ b/src/utils.c @@ -17,124 +17,7 @@ #include #include -// Global flag to disable token expansion -bool zstr_disable_token_expansion = false; - -// Global flag to disable colors (tokens expand to empty strings) -bool zstr_no_colors = false; - -TokenExpansion zstr_expand_tokens_with_cursor(const char *text) { - TokenExpansion result; - result.expanded = zstr_init(); - result.cursor_pos = -1; - - const char *in = text; - int visual_pos = 1; // 1-indexed for terminal positioning - - // If expansion is disabled, just return a copy of the text - if (zstr_disable_token_expansion) { - zstr_cat(&result.expanded, text); - return result; - } - - while (*in) { - if (*in == '{') { - if (strncmp(in, "{cursor}", 8) == 0) { - result.cursor_pos = visual_pos; - in += 8; - } else if (strncmp(in, "{h1}", 4) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, ANSI_BOLD); - zstr_cat(&result.expanded, "\033[38;5;214m"); // Orange - } - in += 4; - } else if (strncmp(in, "{h2}", 4) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, ANSI_BOLD); - zstr_cat(&result.expanded, ANSI_BLUE); - } - in += 4; - } else if (strncmp(in, "{b}", 3) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, ANSI_BOLD); - zstr_cat(&result.expanded, ANSI_YELLOW); - } - in += 3; - } else if (strncmp(in, "{/b}", 4) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, "\033[22m"); // Turn off bold - zstr_cat(&result.expanded, "\033[39m"); // Reset foreground color - } - in += 4; - } else if (strncmp(in, "{dim}", 5) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, "\033[90m"); // Bright black (gray) like Ruby version - } - in += 5; - } else if (strncmp(in, "{reset}", 7) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, ANSI_RESET); - } - in += 7; - } else if (strncmp(in, "{/fg}", 5) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, "\033[39m"); // Reset foreground - } - in += 5; - } else if (strncmp(in, "{text}", 6) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, ANSI_RESET); - } - in += 6; - } else if (strncmp(in, "{section}", 9) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, ANSI_BOLD); - } - in += 9; - } else if (strncmp(in, "{/section}", 10) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, ANSI_RESET); - } - in += 10; - } else if (strncmp(in, "{strike}", 8) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, "\033[48;5;52m"); // Dark red background (#5f0000) - } - in += 8; - } else if (strncmp(in, "{/strike}", 9) == 0) { - if (!zstr_no_colors) { - zstr_cat(&result.expanded, "\033[49m"); // Reset background - } - in += 9; - } else { - zstr_push(&result.expanded, *in++); - visual_pos++; - } - } else if (*in == '\x1b') { - // Skip ANSI escape sequences (don't count toward visual position) - zstr_push(&result.expanded, *in++); - while (*in && !isalpha((unsigned char)*in)) { - zstr_push(&result.expanded, *in++); - } - if (*in) { - zstr_push(&result.expanded, *in++); - } - } else if (*in == '\n') { - // Newlines don't count toward visual position (next line starts at col 1) - zstr_push(&result.expanded, *in++); - visual_pos = 1; - } else { - zstr_push(&result.expanded, *in++); - visual_pos++; - } - } - return result; -} - -zstr zstr_expand_tokens(const char *text) { - TokenExpansion result = zstr_expand_tokens_with_cursor(text); - return result.expanded; -} +// Token expansion is now in tokens.c (generated from tokens.rl) char *trim(char *str) { char *end; diff --git a/src/utils.h b/src/utils.h index bbbad95..77b2e08 100644 --- a/src/utils.h +++ b/src/utils.h @@ -3,6 +3,7 @@ #include "zstr.h" #include "zvec.h" +#include "tokens.h" #include #include #include @@ -11,7 +12,7 @@ Z_VEC_GENERATE_IMPL(zstr, zstr) Z_VEC_GENERATE_IMPL(char *, char_ptr) -// ANSI Colors +// ANSI Colors (legacy defines, prefer using tokens instead) #define ANSI_RESET "\033[0m" #define ANSI_BOLD "\033[1m" #define ANSI_DIM "\033[2m" @@ -27,25 +28,6 @@ Z_VEC_GENERATE_IMPL(char *, char_ptr) static inline void cleanup_free(void *p) { free(*(void **)p); } #define AUTO_FREE Z_CLEANUP(cleanup_free) -// Global flag to disable token expansion (for testing) -extern bool zstr_disable_token_expansion; - -// Global flag to disable colors (tokens expand to empty strings) -extern bool zstr_no_colors; - -// Result of token expansion with optional cursor position -typedef struct { - zstr expanded; - int cursor_pos; // -1 if no cursor marker found, otherwise visual column (1-indexed) -} TokenExpansion; - -// Token expansion for UI -// Returns a zstr that must be freed (or use Z_CLEANUP(zstr_free)) -zstr zstr_expand_tokens(const char *text); - -// Token expansion with cursor tracking -TokenExpansion zstr_expand_tokens_with_cursor(const char *text); - // String helpers char *trim(char *str); // Operates in-place zstr join_path(const char *dir, const char *file);