| name | dailytags |
| description | Comprehensive guide for using DailyTags - a Jetpack Compose markdown library that supports custom tags. Use this skill when working with markdown rendering, custom markup, rich text formatting in Compose, or creating pattern-based text parsers for Android applications. DailyTags parses text into nodes styled with AnnotatedString, providing a WebView-free solution for rendering markdown and HTML with full customization support. |
DailyTags Skill
Overview
DailyTags is a flexible markdown library for Jetpack Compose that parses markup into rich text using AnnotatedString. It's lightweight (<50kb), fast, extensible, and requires no WebView.
Key Capabilities:
- ā
Markdown parsing with built-in rules
- ā
HTML support with built-in rules
- ā
Custom tag/markup creation via regex patterns
- ā
Full styling control (SpanStyle, ParagraphStyle)
- ā
Node-based parsing architecture
- ā
Native Compose integration
Important Limitation:
- ā ļø Text content only - Cannot render images, tables, checkboxes, or custom UI elements
- For non-text elements, use hybrid approach with regular Compose components
Installation
Gradle Setup
Add JitPack repository to your root build.gradle:
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
Add dependency to your module build.gradle:
dependencies {
implementation "com.github.DmytroShuba:DailyTags:1.0.0"
}
Current Version: 1.0.0 (February 2022)
Min API Level: 23+
License: MIT
Core Architecture
Parsing Flow
Source Text ā Rules ā Parser ā Nodes ā Render ā AnnotatedString ā Text Composable
Key Components
- SimpleMarkupParser: Main parsing engine that processes text using rules
- Rules: Pattern-based definitions for how to extract and style content
- Nodes: Internal representation of parsed elements
- AnnotatedString: Compose's rich text format (final output)
Basic Usage
1. Simple Markdown Rendering
import com.dmytroshuba.dailytags.markdown.rules.MarkdownRules
import com.dmytroshuba.dailytags.core.simple.SimpleMarkupParser
import com.dmytroshuba.dailytags.core.simple.render
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun MarkdownText(source: String) {
val parser = SimpleMarkupParser()
val rules = MarkdownRules.toList()
val content = parser
.parse(source, rules)
.render()
.toAnnotatedString()
Text(text = content)
}
MarkdownText("**Bold text** and *italic text*")
2. Markdown + HTML Combined
import com.dmytroshuba.dailytags.markdown.rules.HtmlRules
@Composable
fun RichText(source: String) {
val parser = SimpleMarkupParser()
val rules = MarkdownRules.toList() + HtmlRules.toList()
val content = parser
.parse(source, rules)
.render()
.toAnnotatedString()
Text(text = content)
}
RichText("""
<b>HTML bold</b> and **Markdown bold**
<i>HTML italic</i> and *Markdown italic*
""")
Custom Tags & Markup
Creating Custom Rules
Step 1: Define Pattern
Use Java's Pattern class to create regex patterns:
import java.util.regex.Pattern
val PATTERN_HIGHLIGHT_BLUE = Pattern.compile("^<hl-blue>([\\s\\S]+?)<\\/hl-blue>")
val PATTERN_SPOILER = Pattern.compile("^\\|\\|([\\s\\S]+?)\\|\\|")
val PATTERN_MENTION = Pattern.compile("^@([a-zA-Z0-9_]+)")
val PATTERN_CODE_BLOCK = Pattern.compile("^```(\\w+)\\n([\\s\\S]+?)```")
Step 2: Convert Pattern to Rule
Use the toRule() extension function:
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import com.dmytroshuba.dailytags.core.simple.toRule
val blueHighlightRule = PATTERN_HIGHLIGHT_BLUE.toRule(
spanStyle = SpanStyle(
color = Color.Blue,
background = Color.Blue.copy(alpha = 0.1f)
)
)
val spoilerRule = PATTERN_SPOILER.toRule(
spanStyle = SpanStyle(
color = Color.Black,
background = Color.DarkGray
)
)
val mentionRule = PATTERN_MENTION.toRule(
spanStyle = SpanStyle(
color = Color(0xFF1DA1F2),
fontWeight = FontWeight.Bold
)
)
Step 3: Use Custom Rules
@Composable
fun CustomMarkupText(source: String) {
val parser = SimpleMarkupParser()
val rules = MarkdownRules.toList() + listOf(
blueHighlightRule,
spoilerRule,
mentionRule
)
val content = parser
.parse(source, rules)
.render()
.toAnnotatedString()
Text(text = content)
}
CustomMarkupText("""
Regular text with <hl-blue>blue highlight</hl-blue>
This is a ||spoiler||
Hey @username, check this out!
""")
Advanced Patterns
1. Paragraph Styling
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.style.TextIndent
val PATTERN_QUOTE = Pattern.compile("^> (.+)$", Pattern.MULTILINE)
val quoteRule = PATTERN_QUOTE.toRule(
spanStyle = SpanStyle(
color = Color.Gray,
fontStyle = FontStyle.Italic
),
paragraphStyle = ParagraphStyle(
textIndent = TextIndent(firstLine = 16.sp),
lineHeight = 20.sp
)
)
2. Multi-Group Patterns
Extract and style different groups separately:
val PATTERN_LINK = Pattern.compile("^\\[([^\\]]+)\\]\\(([^)]+)\\)")
val linkRule = PATTERN_LINK.toRule(
spanStyle = SpanStyle(
color = Color.Blue,
textDecoration = TextDecoration.Underline
)
)
3. Nested Rules
Rules are processed in order, allowing nested markup:
val rules = listOf(
blockquoteRule,
boldRule,
italicRule,
codeRule
)
4. Conditional Styling
Create rules that apply different styles based on content:
val PATTERN_TAG = Pattern.compile("^#(\\w+)")
fun createTagRule(color: Color) = PATTERN_TAG.toRule(
spanStyle = SpanStyle(
color = color,
fontWeight = FontWeight.Medium,
background = color.copy(alpha = 0.1f)
)
)
val tagRules = listOf(
createTagRule(Color.Red),
createTagRule(Color.Blue),
createTagRule(Color.Green)
)
Important Limitations
DailyTags works with TEXT CONTENT ONLY. It cannot render:
- ā Images (including
 and <img> tags)
- ā Tables
- ā Checkboxes / Task lists
- ā Custom UI elements
- ā Embedded media (video, audio)
- ā Interactive widgets
What this means:
- Image syntax like
 will be parsed but NOT display an image
- Table markdown will be treated as plain text
- Checkbox syntax
- [ ] will render as text, not interactive checkboxes
- Only text styling (bold, italic, color, etc.) is supported
For these features, you need to:
- Use regular Compose Image() composables separately
- Build custom UI components outside DailyTags
- Consider hybrid approaches (DailyTags for text, Compose for UI elements)
Supported Markdown Features
Text Formatting
- Bold:
**text** or __text__
- Italic:
*text* or _text_
Strikethrough: ~~text~~
Inline code: `code`
- Combined:
***bold italic***
Headings
# H1 Heading
## H2 Heading
### H3 Heading
#### H4 Heading
##### H5 Heading
###### H6 Heading
Lists
- Unordered list item 1
- Unordered list item 2
- Nested item
1. Ordered list item 1
2. Ordered list item 2
1. Nested numbered item
Links and Images (Text Only)
Links:
[Link text](https://example.com)
Note: Links are styled but not clickable by default. Use ClickableText for interactivity.
Images:

ā ļø Important: Image markdown is parsed as text only - no actual image rendering. You'll see the alt text, not the image. Use Compose's Image() composable separately if you need to display images.
Code Blocks
```
Code block
```
```kotlin
fun example() {
println("Language-specific")
}
```
Blockquotes
> This is a quote
> It can span multiple lines
Horizontal Rules
---
***
___
Supported HTML Tags
When using HtmlRules:
Text Formatting:
<b> / <strong> - Bold
<i> / <em> - Italic
<u> - Underline
<s> / <strike> - Strikethrough
<code> - Inline code
<pre> - Preformatted text
Structure:
<h1> to <h6> - Headings
<p> - Paragraphs
<br> / <br/> - Line breaks
<ul>, <ol>, <li> - Lists
<blockquote> - Quotes
Links (Text Only):
<a href=""> - Links (styled but not clickable by default)
Not Rendered:
<img src=""> - ā ļø Parsed but does NOT display images
<table>, <tr>, <td> - ā ļø Not supported, treated as plain text
<video>, <audio> - ā ļø Not supported
<input>, <button> - ā ļø Not supported
Best Practices
1. Hybrid Approach for Images and UI Elements
Since DailyTags only handles text, combine it with regular Compose for non-text elements:
@Composable
fun RichContent(markdown: String, images: List<ImageData>) {
Column {
val textContent = parseMarkdown(markdown)
Text(text = textContent)
images.forEach { imageData ->
AsyncImage(
model = imageData.url,
contentDescription = imageData.alt,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
}
}
}
For tables, use custom Compose layouts:
@Composable
fun MarkdownWithTable(
beforeTable: String,
tableData: TableData,
afterTable: String
) {
Column {
MarkdownText(beforeTable)
TableComposable(tableData)
MarkdownText(afterTable)
}
}
For checkboxes/task lists:
@Composable
fun TaskList(tasks: List<Task>) {
tasks.forEach { task ->
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = task.completed,
onCheckedChange = { task.onToggle() }
)
MarkdownText(task.description)
}
}
}
2. Rule Organization
object CustomMarkupRules {
private val PATTERN_HIGHLIGHT = Pattern.compile("...")
private val PATTERN_SPOILER = Pattern.compile("...")
private val PATTERN_MENTION = Pattern.compile("...")
private val highlightRule = PATTERN_HIGHLIGHT.toRule(...)
private val spoilerRule = PATTERN_SPOILER.toRule(...)
private val mentionRule = PATTERN_MENTION.toRule(...)
fun toList() = listOf(
highlightRule,
spoilerRule,
mentionRule
)
}
val rules = MarkdownRules.toList() + CustomMarkupRules.toList()
2. Parser Reuse
Cache the parser instance for better performance:
class MarkdownViewModel : ViewModel() {
private val parser = SimpleMarkupParser()
private val rules = MarkdownRules.toList()
fun parseMarkdown(source: String): AnnotatedString {
return parser
.parse(source, rules)
.render()
.toAnnotatedString()
}
}
3. Theme Integration
Adapt styling to Material Theme:
@Composable
fun ThemedMarkdown(source: String) {
val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography
val customRules = remember(colorScheme, typography) {
listOf(
createCodeRule(colorScheme.primaryContainer),
createLinkRule(colorScheme.primary),
createQuoteRule(colorScheme.outline)
)
}
val allRules = MarkdownRules.toList() + customRules
}
4. Performance Optimization
For long documents, consider lazy rendering:
@Composable
fun LongMarkdownDocument(source: String) {
val sections = remember(source) {
source.split("\n\n")
}
LazyColumn {
items(sections) { section ->
MarkdownText(section)
}
}
}
5. Interactive Elements
Combine with clickable modifiers:
val PATTERN_CLICKABLE = Pattern.compile("^\\[\\[([^\\]]+)\\]\\]")
@Composable
fun InteractiveMarkdown(
source: String,
onLinkClick: (String) -> Unit
) {
val annotatedString = parser
.parse(source, rules)
.render()
.toAnnotatedString()
ClickableText(
text = annotatedString,
onClick = { offset ->
annotatedString.getStringAnnotations(
tag = "URL",
start = offset,
end = offset
).firstOrNull()?.let { annotation ->
onLinkClick(annotation.item)
}
}
)
}
Common Patterns & Examples
1. Chat Message Formatting
val PATTERN_USER_MENTION = Pattern.compile("^@([a-zA-Z0-9_]+)")
val PATTERN_CHANNEL = Pattern.compile("^#([a-zA-Z0-9_-]+)")
val PATTERN_EMOJI = Pattern.compile("^:([a-zA-Z0-9_]+):")
val chatRules = listOf(
PATTERN_USER_MENTION.toRule(
spanStyle = SpanStyle(
color = Color(0xFF1DA1F2),
fontWeight = FontWeight.Bold
)
),
PATTERN_CHANNEL.toRule(
spanStyle = SpanStyle(
color = Color(0xFF4A90E2),
fontWeight = FontWeight.Medium
)
),
PATTERN_EMOJI.toRule(
spanStyle = SpanStyle(fontSize = 20.sp)
)
) + MarkdownRules.toList()
2. Code Documentation
val PATTERN_PARAM = Pattern.compile("^@param\\s+(\\w+)\\s+(.+)$", Pattern.MULTILINE)
val PATTERN_RETURN = Pattern.compile("^@return\\s+(.+)$", Pattern.MULTILINE)
val docRules = listOf(
PATTERN_PARAM.toRule(
spanStyle = SpanStyle(
color = Color(0xFF569CD6),
fontFamily = FontFamily.Monospace
)
),
PATTERN_RETURN.toRule(
spanStyle = SpanStyle(
color = Color(0xFF4EC9B0),
fontFamily = FontFamily.Monospace
)
)
) + MarkdownRules.toList()
3. Syntax Highlighting Preview
val PATTERN_KEYWORD = Pattern.compile("^\\b(fun|val|var|class|object)\\b")
val PATTERN_STRING = Pattern.compile("^\"([^\"]*?)\"")
val PATTERN_COMMENT = Pattern.compile("^//(.+)$", Pattern.MULTILINE)
val syntaxRules = listOf(
PATTERN_KEYWORD.toRule(
spanStyle = SpanStyle(
color = Color(0xFFCC7832),
fontWeight = FontWeight.Bold
)
),
PATTERN_STRING.toRule(
spanStyle = SpanStyle(color = Color(0xFF6A8759))
),
PATTERN_COMMENT.toRule(
spanStyle = SpanStyle(
color = Color(0xFF808080),
fontStyle = FontStyle.Italic
)
)
)
4. Social Media Post
val PATTERN_HASHTAG = Pattern.compile("^#(\\w+)")
val PATTERN_URL = Pattern.compile("^(https?://[^\\s]+)")
val socialRules = listOf(
PATTERN_HASHTAG.toRule(
spanStyle = SpanStyle(
color = Color(0xFF1DA1F2),
fontWeight = FontWeight.Medium
)
),
PATTERN_URL.toRule(
spanStyle = SpanStyle(
color = Color(0xFF1DA1F2),
textDecoration = TextDecoration.Underline
)
)
) + MarkdownRules.toList()
5. Math Notation (Simple)
val PATTERN_INLINE_MATH = Pattern.compile("^\\$([^\\$]+)\\$")
val PATTERN_BLOCK_MATH = Pattern.compile("^\\$\\$([\\s\\S]+?)\\$\\$")
val mathRules = listOf(
PATTERN_INLINE_MATH.toRule(
spanStyle = SpanStyle(
fontFamily = FontFamily.Serif,
fontStyle = FontStyle.Italic,
color = Color(0xFF8B4513)
)
),
PATTERN_BLOCK_MATH.toRule(
spanStyle = SpanStyle(
fontFamily = FontFamily.Serif,
fontSize = 18.sp
),
paragraphStyle = ParagraphStyle(
textAlign = TextAlign.Center
)
)
)
Troubleshooting
Pattern Not Matching
Problem: Custom pattern doesn't match expected text
Solutions:
- Test regex separately:
val pattern = Pattern.compile("your pattern here")
val matcher = pattern.matcher("test text")
if (matcher.find()) {
println("Match: ${matcher.group()}")
} else {
println("No match")
}
- Use proper flags:
Pattern.compile("pattern", Pattern.CASE_INSENSITIVE)
Pattern.compile("pattern", Pattern.MULTILINE)
Pattern.compile("pattern", Pattern.DOTALL)
Pattern.compile("pattern", Pattern.CASE_INSENSITIVE or Pattern.MULTILINE)
- Escape special characters:
Pattern.compile("^\\$")
Rules Not Applied
Problem: Rules defined but styling not appearing
Checklist:
- ā Rules added to parser:
parser.parse(source, rules)
- ā Rule order matters: More specific rules should come before general ones
- ā Pattern starts with
^: All patterns must start with ^ anchor
- ā Called
.render().toAnnotatedString(): Both methods required
- ā SpanStyle vs ParagraphStyle: Use appropriate style type
Performance Issues
Problem: Slow rendering for large documents
Solutions:
- Split content into sections and render lazily:
LazyColumn {
items(sections) { section ->
MarkdownSection(section)
}
}
- Cache parsed results:
val cachedContent = remember(source) {
parser.parse(source, rules)
.render()
.toAnnotatedString()
}
- Limit rule complexity - simpler patterns parse faster
Styling Not Visible
Problem: Styles defined but not visible in UI
Debug steps:
- Check Text composable properties:
Text(
text = content,
color = Color.Unspecified,
fontSize = TextUnit.Unspecified
)
- Verify SpanStyle colors contrast with background
- Check MaterialTheme isn't overriding colors
Nested Tags Not Working
Problem: Tags inside tags not parsing correctly
Solution: Order rules from outermost to innermost:
val rules = listOf(
blockquoteRule,
boldRule,
italicRule,
inlineCodeRule
)
Migration & Version Notes
From Plain Text:
- Simply wrap existing text with DailyTags parser
- No changes needed to text format for basic markdown
From WebView:
- Remove WebView dependencies
- Replace with DailyTags parser
- Significant performance improvement
- Better integration with Compose UI
From Other Markdown Libraries:
- Check supported features list (not all markdown specs supported)
- Some features may need custom rules
- Pattern syntax uses Java regex
Resources
Quick Reference
Essential Imports
import com.dmytroshuba.dailytags.core.simple.SimpleMarkupParser
import com.dmytroshuba.dailytags.core.simple.render
import com.dmytroshuba.dailytags.core.simple.toRule
import com.dmytroshuba.dailytags.markdown.rules.MarkdownRules
import com.dmytroshuba.dailytags.markdown.rules.HtmlRules
import java.util.regex.Pattern
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.ParagraphStyle
Minimal Working Example
@Composable
fun MinimalMarkdown() {
val source = "**Bold** and *italic* text"
val parser = SimpleMarkupParser()
val content = parser
.parse(source, MarkdownRules.toList())
.render()
.toAnnotatedString()
Text(text = content)
}
Complete Custom Rule Example
val PATTERN = Pattern.compile("^<tag>([\\s\\S]+?)</tag>")
val rule = PATTERN.toRule(
spanStyle = SpanStyle(
color = Color.Red,
fontWeight = FontWeight.Bold
)
)
val content = SimpleMarkupParser()
.parse(source, listOf(rule))
.render()
.toAnnotatedString()
Text(text = content)
Remember
- Text content only - No images, tables, checkboxes, or custom UI elements
- Patterns must start with
^ - This is mandatory for DailyTags
- Rule order matters - Process outer/general rules before inner/specific ones
- Both render() and toAnnotatedString() required - Don't skip either step
- Use remember() for performance - Cache parsed results when possible
- Test patterns independently - Verify regex before creating rules
- SpanStyle for inline, ParagraphStyle for blocks - Choose the right style type
- Combine built-in rules freely - MarkdownRules + HtmlRules + CustomRules
- Library is lightweight - No WebView overhead, pure Compose
- Use hybrid approach - Combine DailyTags text rendering with regular Compose UI for non-text elements