Skip to content

umono-cms/compono

Compono

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.

Installation

go get github.com/umono-cms/compono

Quick Start

package 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>
}

Syntax

Markdown Support

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

Components are the core feature of Compono. They allow you to create reusable content blocks.

Defining a Local Component

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 with Parameters

Components can accept parameters with default values:

{{ USER_CARD name="Anonymous" role="Guest" }}

~ USER_CARD name="" role=""
## {{ name }}
*{{ role }}*

Block vs Inline Components

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

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 }}`))

Built-in Components

LINK

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>

Parameters

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 Types

Supported parameter types:

  • Stringname = "John"
  • Numberage = 25
  • Boolactive = true
  • Componentcomp = COMP
  • Arrayitems = ["Jane", 22, true, COMP]
  • Recordconfig = { lang: "tr", for-admin: true }

Passing Parameters to Other Components

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:

  • USER receives age
  • it forwards that value to ANOTHER_COMP

Passing Components as Parameters

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-wrapper receives a component
  • that component is executed inside USER

Global Parameter Visibility in Local Components

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.


Array Parameters

{{ 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] }}

Record Parameters

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 }}

Error Handling

Compono provides error feedback by rendering placeholders where errors occur. Fatal errors during conversion stop the process and no output is produced.

API Reference

Core Methods

// 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 Naming Convention

Component names must be in SCREAMING_SNAKE_CASE:

  • HEADER
  • USER_PROFILE
  • NAV_MENU_ITEM
  • header
  • userProfile

Parameter Naming Convention

Parameter names must be in kebab-case:

  • name
  • user-name
  • is-active
  • userName
  • user_name

Component Override Behavior

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.

License

MIT License - see LICENSE for details.

About

Component-based Markdown DSL

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages