From daily-tags
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.
npx claudepluginhub jayteealao/agent-skills --plugin daily-tagsThis skill uses the workspace's default tool permissions.
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.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
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:
Important Limitation:
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
Source Text → Rules → Parser → Nodes → Render → AnnotatedString → Text Composable
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)
}
// Usage
MarkdownText("**Bold text** and *italic text*")
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)
}
// Usage
RichText("""
<b>HTML bold</b> and **Markdown bold**
<i>HTML italic</i> and *Markdown italic*
""")
Use Java's Pattern class to create regex patterns:
import java.util.regex.Pattern
// Highlight tag: <hl-blue>text</hl-blue>
val PATTERN_HIGHLIGHT_BLUE = Pattern.compile("^<hl-blue>([\\s\\S]+?)<\\/hl-blue>")
// Spoiler tag: ||hidden text||
val PATTERN_SPOILER = Pattern.compile("^\\|\\|([\\s\\S]+?)\\|\\|")
// Mention tag: @username
val PATTERN_MENTION = Pattern.compile("^@([a-zA-Z0-9_]+)")
// Code language tag: ```kotlin code ```
val PATTERN_CODE_BLOCK = Pattern.compile("^```(\\w+)\\n([\\s\\S]+?)```")
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
// Basic styling
val blueHighlightRule = PATTERN_HIGHLIGHT_BLUE.toRule(
spanStyle = SpanStyle(
color = Color.Blue,
background = Color.Blue.copy(alpha = 0.1f)
)
)
// Complex styling with multiple properties
val spoilerRule = PATTERN_SPOILER.toRule(
spanStyle = SpanStyle(
color = Color.Black,
background = Color.DarkGray
)
)
val mentionRule = PATTERN_MENTION.toRule(
spanStyle = SpanStyle(
color = Color(0xFF1DA1F2), // Twitter blue
fontWeight = FontWeight.Bold
)
)
@Composable
fun CustomMarkupText(source: String) {
val parser = SimpleMarkupParser()
// Combine built-in and custom rules
val rules = MarkdownRules.toList() + listOf(
blueHighlightRule,
spoilerRule,
mentionRule
)
val content = parser
.parse(source, rules)
.render()
.toAnnotatedString()
Text(text = content)
}
// Usage
CustomMarkupText("""
Regular text with <hl-blue>blue highlight</hl-blue>
This is a ||spoiler||
Hey @username, check this out!
""")
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
)
)
Extract and style different groups separately:
// Pattern with capturing groups
val PATTERN_LINK = Pattern.compile("^\\[([^\\]]+)\\]\\(([^)]+)\\)")
// Custom rendering with different styles for text and URL
val linkRule = PATTERN_LINK.toRule(
spanStyle = SpanStyle(
color = Color.Blue,
textDecoration = TextDecoration.Underline
)
)
Rules are processed in order, allowing nested markup:
val rules = listOf(
// Process outer tags first
blockquoteRule,
// Then inner formatting
boldRule,
italicRule,
codeRule
)
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)
)
)
// Usage with different colors
val tagRules = listOf(
createTagRule(Color.Red),
createTagRule(Color.Blue),
createTagRule(Color.Green)
)
DailyTags works with TEXT CONTENT ONLY. It cannot render:
 and <img> tags)What this means:
 will be parsed but NOT display an image- [ ] will render as text, not interactive checkboxesFor these features, you need to:
**text** or __text__*text* or _text_~~text~~Inline code: `code`***bold italic***# H1 Heading
## H2 Heading
### H3 Heading
#### H4 Heading
##### H5 Heading
###### H6 Heading
- 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:
[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 block
```
```kotlin
fun example() {
println("Language-specific")
}
```
> This is a quote
> It can span multiple lines
---
***
___
When using HtmlRules:
Text Formatting:
<b> / <strong> - Bold<i> / <em> - Italic<u> - Underline<s> / <strike> - Strikethrough<code> - Inline code<pre> - Preformatted textStructure:
<h1> to <h6> - Headings<p> - Paragraphs<br> / <br/> - Line breaks<ul>, <ol>, <li> - Lists<blockquote> - QuotesLinks (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 supportedSince DailyTags only handles text, combine it with regular Compose for non-text elements:
@Composable
fun RichContent(markdown: String, images: List<ImageData>) {
Column {
// Render markdown text
val textContent = parseMarkdown(markdown)
Text(text = textContent)
// Render images separately
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)
// Custom table rendering
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() }
)
// Use DailyTags for task description
MarkdownText(task.description)
}
}
}
object CustomMarkupRules {
// Define all patterns
private val PATTERN_HIGHLIGHT = Pattern.compile("...")
private val PATTERN_SPOILER = Pattern.compile("...")
private val PATTERN_MENTION = Pattern.compile("...")
// Define all rules
private val highlightRule = PATTERN_HIGHLIGHT.toRule(...)
private val spoilerRule = PATTERN_SPOILER.toRule(...)
private val mentionRule = PATTERN_MENTION.toRule(...)
// Public function to get all rules
fun toList() = listOf(
highlightRule,
spoilerRule,
mentionRule
)
}
// Usage
val rules = MarkdownRules.toList() + CustomMarkupRules.toList()
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()
}
}
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
// ... parse and render
}
For long documents, consider lazy rendering:
@Composable
fun LongMarkdownDocument(source: String) {
val sections = remember(source) {
source.split("\n\n") // Split by paragraphs
}
LazyColumn {
items(sections) { section ->
MarkdownText(section)
}
}
}
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 ->
// Handle clicks on links
annotatedString.getStringAnnotations(
tag = "URL",
start = offset,
end = offset
).firstOrNull()?.let { annotation ->
onLinkClick(annotation.item)
}
}
)
}
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()
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()
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
)
)
)
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()
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
)
)
)
Problem: Custom pattern doesn't match expected text
Solutions:
val pattern = Pattern.compile("your pattern here")
val matcher = pattern.matcher("test text")
if (matcher.find()) {
println("Match: ${matcher.group()}")
} else {
println("No match")
}
// Case insensitive
Pattern.compile("pattern", Pattern.CASE_INSENSITIVE)
// Multiline mode
Pattern.compile("pattern", Pattern.MULTILINE)
// Dot matches newlines
Pattern.compile("pattern", Pattern.DOTALL)
// Combine flags
Pattern.compile("pattern", Pattern.CASE_INSENSITIVE or Pattern.MULTILINE)
// Wrong: Pattern.compile("^$")
// Correct:
Pattern.compile("^\\$")
Problem: Rules defined but styling not appearing
Checklist:
parser.parse(source, rules)^: All patterns must start with ^ anchor.render().toAnnotatedString(): Both methods requiredProblem: Slow rendering for large documents
Solutions:
LazyColumn {
items(sections) { section ->
MarkdownSection(section)
}
}
val cachedContent = remember(source) {
parser.parse(source, rules)
.render()
.toAnnotatedString()
}
Problem: Styles defined but not visible in UI
Debug steps:
Text(
text = content,
// Ensure these don't override AnnotatedString styling
color = Color.Unspecified, // Don't override
fontSize = TextUnit.Unspecified // Don't override
)
Problem: Tags inside tags not parsing correctly
Solution: Order rules from outermost to innermost:
val rules = listOf(
blockquoteRule, // Outer
boldRule, // Middle
italicRule, // Middle
inlineCodeRule // Inner
)
From Plain Text:
From WebView:
From Other Markdown Libraries:
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
@Composable
fun MinimalMarkdown() {
val source = "**Bold** and *italic* text"
val parser = SimpleMarkupParser()
val content = parser
.parse(source, MarkdownRules.toList())
.render()
.toAnnotatedString()
Text(text = content)
}
// 1. Pattern
val PATTERN = Pattern.compile("^<tag>([\\s\\S]+?)</tag>")
// 2. Rule
val rule = PATTERN.toRule(
spanStyle = SpanStyle(
color = Color.Red,
fontWeight = FontWeight.Bold
)
)
// 3. Parse
val content = SimpleMarkupParser()
.parse(source, listOf(rule))
.render()
.toAnnotatedString()
// 4. Display
Text(text = content)
^ - This is mandatory for DailyTags