Compono is a platform-agnostic, component-based domain-specific language (DSL) that extends Markdown syntax with reusable components.
Originally developed for Umono CMS, Compono can be used in any Go project that needs a flexible templating solution.
go get github.com/umono-cms/componopackage main
import (
"bytes"
"fmt"
"github.com/umono-cms/compono"
)
func main() {
c := compono.New()
source := []byte(`{{ SAY_HELLO name="World" }}
~ SAY_HELLO name="Guest"
# Hello, {{ name }}!
`)
var buf bytes.Buffer
if err := c.Convert(source, &buf); err != nil {
panic(err)
}
fmt.Println(buf.String())
// Output: <h1>Hello, World!</h1>
}Compono supports common Markdown elements:
# Heading 1
## Heading 2
### Heading 3
This is a paragraph with **bold** and *italic* text.
`inline code`
[Link text](https://example.com)
Code blocks are also supported:
```go
fmt.Println("Hello")
```
Components are the core feature of Compono. They allow you to create reusable content blocks.
Local components are defined in the same scope where they're used:
{{ GREETING }}
~ GREETING
Welcome to our website!
The ~ COMPONENT_NAME syntax marks the beginning of a local component definition. Everything after it becomes the component's content. A component definition ends when another component definition starts or at EOF.
Components can accept parameters with default values:
{{ USER_CARD name="Anonymous" role="Guest" }}
~ USER_CARD name="" role=""
## {{ name }}
*{{ role }}*
Components containing multiple paragraphs or block elements are block components:
{{ ARTICLE }}
~ ARTICLE
# Title
First paragraph.
Second paragraph.
Components with single-line content can be used inline:
Welcome, {{ USERNAME }}!
~ USERNAME
John
Global components can be registered once and used across multiple conversions:
c := compono.New()
// Register a global component
c.RegisterGlobalComponent("FOOTER", []byte(`© 2026 My Company`))
// Use it in any conversion
c.Convert([]byte(`
# Page Title
Content here...
{{ FOOTER }}
`), &buf)Global components can also have parameters:
c.RegisterGlobalComponent("BLOG_PAGE", []byte(`title="" content=""
## {{ title }}
{{ content }}`))Creates an anchor element with optional target blank:
{{ LINK text="Visit us" url="https://example.com" new-tab=true }}
Output:
<a href="https://example.com" target="_blank" rel="noopener noreferrer">Visit us</a>Components can accept parameters. Each parameter must have a default value defined in the component definition.
If a parameter value is not provided during the call, the default value is used.
{{ SAY_HELLO name="Jane" }}
~ SAY_HELLO name="John"
# Hello, {{ name }}!
Supported parameter types:
- String →
name = "John" - Number →
age = 25 - Bool →
active = true - Component →
comp = COMP - Array →
items = ["Jane", 22, true, COMP] - Record →
config = { lang: "tr", for-admin: true }
A parameter can be passed directly to another component call.
{{ USER age=31 }}
~ USER age=18
{{ ANOTHER_COMP another-number-param=age }}
~ ANOTHER_COMP another-number-param=0
Number: *{{ another-number-param }}*
Here:
USERreceivesage- it forwards that value to
ANOTHER_COMP
Components themselves can also be passed as parameters.
{{ USER name="Yunus Emre" age=31 age-wrapper=AGE_WRAPPER_2 }}
~ USER name="John" age=25 age-wrapper=AGE_WRAPPER_1
# Welcome **{{ name }}**!
{{ age-wrapper age=age }}
~ AGE_WRAPPER_1 age=0
Your age: *{{ age }}*
~ AGE_WRAPPER_2 age=0
*{{ age }}*
Here:
age-wrapperreceives a component- that component is executed inside
USER
When a global component defines parameters, those parameters are visible to local components inside it.
c.RegisterGlobalComponent("PROFILE_PAGE", []byte(`
name="Guest"
{{ PROFILE_CARD }}
~ PROFILE_CARD
## {{ name }}
Welcome to the profile page.
`))
Usage:
{{ PROFILE_PAGE name="Yunus" }}
Output:
<h2>Yunus</h2>
<p>Welcome to the profile page.</p>
The local component PROFILE_CARD can directly access the global parameter name.
{{ WRAPPER names = ["John", "Jane"] }}
~ WRAPPER names = []
{{ SAY_HELLO name = names[0] }}
{{ SAY_HELLO name = names[1] }}
~ SAY_HELLO name = ""
# Hello **{{ name }}**!
Arrays do not have to be homogeneous.
~ COMP mix = ["Jane", 22, true, SAY_HELLO]
We can reach an element via index.
{{ mix[2] }}
// true
Arrays can be nested.
{{ TABLE data = [
[1,2],
[3,4],
]}}
~ TABLE data = []
{{ data[0][0] }} - {{ data[0][1] }}
{{ data[1][0] }} - {{ data[1][1] }}
Pass data as key - value
{{ COMP record = { title: "Hello", content: "Here Content" } }}
~ COMP record = {}
# {{ record.title }}
{{ record.content }}
Records can be nested
{{ COMP nested = {record: {key-1: "string", key-2: 123}, empty-record: {} } }}
~ COMP nested = {}
{{ nested.record.key-1 }} - {{ nested.record.key-2 }}
Compono provides error feedback by rendering placeholders where errors occur. Fatal errors during conversion stop the process and no output is produced.
// Create a new Compono instance
c := compono.New()
// Convert source to HTML
err := c.Convert(source []byte, writer io.Writer)
// Register a global component
err := c.RegisterGlobalComponent(name string, source []byte)
// Unregister a global component
err := c.UnregisterGlobalComponent(name string)
// Convert and preview a global component
err := c.ConvertGlobalComponent(name string, source []byte, writer io.Writer)Component names must be in SCREAMING_SNAKE_CASE:
- ✓
HEADER - ✓
USER_PROFILE - ✓
NAV_MENU_ITEM - ✗
header - ✗
userProfile
Parameter names must be in kebab-case:
- ✓
name - ✓
user-name - ✓
is-active - ✗
userName - ✗
user_name
When multiple components share the same name, Compono follows a clear override hierarchy:
Local Component > Global Component > Built-in Component
Local always wins:
{{ LINK }}
~ LINK
I override the built-in LINK component!
This outputs <p>I override the built-in LINK component!</p> instead of an anchor tag.
Global overrides built-in:
c.RegisterGlobalComponent("LINK", []byte(`Custom link behavior`))Now all {{ LINK }} calls will use your global definition instead of the built-in one.
This allows you to customize or extend built-in components without modifying the library.
MIT License - see LICENSE for details.