Text Formatting
The Signal Bot supports Markdown-style text formatting in all outgoing messages. This allows bot responses to include bold, italic, monospace, strikethrough, and spoiler text that renders natively in the Signal app.
Supported Styles
Section titled “Supported Styles”| Style | Markdown Syntax | Example | Result |
|---|---|---|---|
| Bold | **text** | **important** | important |
| Italic | *text* | *emphasis* | emphasis |
| Monospace | `text` | `code` | code |
| Strikethrough | ~text~ | ~removed~ | |
| Spoiler | ||text|| | ||hidden|| | (tap to reveal) |
How It Works
Section titled “How It Works”The Problem
Section titled “The Problem”Signal doesn’t parse Markdown syntax like Discord or Slack. If you send **bold** to Signal, users see the raw asterisks. Signal requires text styling to be specified as byte-indexed ranges in a separate textStyles parameter.
The Solution
Section titled “The Solution”The bot includes a Markdown-to-Signal parser that:
- Parses Markdown syntax in outgoing messages
- Calculates byte positions for each styled segment (important for UTF-8/emoji support)
- Strips Markdown markers from the visible text
- Sends the cleaned text with a
textStylesarray to signal-cli
Signal’s textStyles Format
Section titled “Signal’s textStyles Format”Signal-cli’s JSON-RPC API expects styles as an array of strings in the format "start:length:STYLE":
{ "method": "send", "params": { "message": "This is bold text", "textStyles": ["8:4:BOLD"] }}This tells Signal: starting at byte 8, for 4 bytes, apply BOLD style.
Parser Implementation
Section titled “Parser Implementation”File: src/src/bot/signal-bot-v2.ts - parseMarkdownToSignalStyles()
Function Signature
Section titled “Function Signature”function parseMarkdownToSignalStyles(text: string): { text: string; // Cleaned text with markers removed textStyles: TextStyle[]; // Array of style ranges}
interface TextStyle { start: number; // Byte offset (not character index) length: number; // Length in bytes style: 'BOLD' | 'ITALIC' | 'STRIKETHROUGH' | 'SPOILER' | 'MONOSPACE';}Processing Order
Section titled “Processing Order”Styles are processed in this order to avoid conflicts:
- Bold (
**text**) - Most specific, processed first - Italic (
*text*) - Uses negative lookbehind to avoid matching inside** - Monospace (
`text`) - Strikethrough (
~text~) - Spoiler (
||text||)
Byte Offset Calculation
Section titled “Byte Offset Calculation”The parser calculates byte offsets, not character indices. This is critical for:
- Emoji (most are 4 bytes)
- Non-ASCII characters (2-4 bytes in UTF-8)
- Mixed content
const getByteOffset = (str: string, charIndex: number): number => { return Buffer.byteLength(str.substring(0, charIndex), 'utf8');};Testing
Section titled “Testing”Via Deploy Script
Section titled “Via Deploy Script”Run the formatting test to send a test message to the Bot Development room:
./deploy.sh test-formattingThis will:
- Show the original message with Markdown markers
- Show the cleaned message
- List all detected styles with byte positions
- Send the message to verify rendering
Test File
Section titled “Test File”File: src/src/utils/formatting-test.ts
Contains a standalone test that can be run to verify the parser is working correctly.
Usage in Bot Code
Section titled “Usage in Bot Code”All messages sent via bot.sendMessage() are automatically parsed:
// In command-handler.ts or any bot codeawait this.bot.sendMessage({ groupId: context.groupId, message: '**Success!** Your question has been recorded.\n\nUse `!answer` to respond.'});The parser handles the conversion automatically - developers write Markdown, users see formatted text.
Known Limitations
Section titled “Known Limitations”- Nested styles partially supported -
**Bold withcodeinside**works, but deeply nested combinations may have edge cases - No block quotes - Signal doesn’t support block quote styling
- No headers - Signal doesn’t support header sizes
- No links - Signal auto-links URLs but doesn’t support
[text](url)syntax
Troubleshooting
Section titled “Troubleshooting”Raw asterisks showing instead of bold
Section titled “Raw asterisks showing instead of bold”Cause: The textStyles parameter wasn’t being sent correctly.
History: Fixed on 2025-12-21 - the parameter name was textStyle (singular) but signal-cli expects textStyles (plural). See LESSONS_LEARNED.md.
Styles appear in wrong position
Section titled “Styles appear in wrong position”Cause: Character index used instead of byte offset.
Fix: Ensure Buffer.byteLength() is used for position calculation, not string.length.
Mixed emoji and text breaks styling
Section titled “Mixed emoji and text breaks styling”Cause: Emoji byte lengths vary (most are 4 bytes).
Fix: The parser handles this correctly by using byte offsets. If you see issues, check that the parser is processing the text before any emoji manipulation.
Related Documentation
Section titled “Related Documentation”- SIGNAL_BOT_STYLE_UX_GUIDE.md - Full UX patterns and formatting standards
- Core Commands - Command reference
- LESSONS_LEARNED.md - Bug fixes and learnings