<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Tooling on despatches</title><link>https://icle.es/tags/tooling/</link><description>Recent content in Tooling on despatches</description><generator>Hugo</generator><language>en</language><lastBuildDate>Wed, 18 Mar 2026 20:33:52 +0000</lastBuildDate><atom:link href="https://icle.es/tags/tooling/index.xml" rel="self" type="application/rss+xml"/><item><title>Inscribe: Updating frontmatter in-place with Go and yaml.Node</title><link>https://icle.es/2025/07/05/inscribe-updating-frontmatter-in-place-with-go-and-yaml.node/</link><pubDate>Sat, 05 Jul 2025 10:44:02 +0100</pubDate><guid>https://icle.es/2025/07/05/inscribe-updating-frontmatter-in-place-with-go-and-yaml.node/</guid><description>&lt;p>While building
&lt;a href="https://icle.es/wordsonsand/projector-sync.md">a little tool to sync up YouTube video descriptions from hugo&lt;/a>,
I needed a library to read and write frontmatter in yaml.&lt;/p>
&lt;p>I started with &lt;a href="https://github.com/adrg/frontmatter">adrg/frontmatter&lt;/a> before I
realised that it didn&amp;rsquo;t have the ability to write back.&lt;/p>
&lt;p>I considered contributing to that one, but writing back is a little more complex
than reading - particularly because the &lt;code>frontmatter.Parse&lt;/code> in that one is built
to support partial reading.&lt;/p>
&lt;p>Because adrg/frontmatter only unmarshals into a struct, and doesn’t store the
original bytes, you can’t write back without losing untouched keys.&lt;/p></description><content:encoded><![CDATA[<p>While building
<a href="https://icle.es/wordsonsand/projector-sync.md">a little tool to sync up YouTube video descriptions from hugo</a>,
I needed a library to read and write frontmatter in yaml.</p>
<p>I started with <a href="https://github.com/adrg/frontmatter">adrg/frontmatter</a> before I
realised that it didn&rsquo;t have the ability to write back.</p>
<p>I considered contributing to that one, but writing back is a little more complex
than reading - particularly because the <code>frontmatter.Parse</code> in that one is built
to support partial reading.</p>
<p>Because adrg/frontmatter only unmarshals into a struct, and doesn’t store the
original bytes, you can’t write back without losing untouched keys.</p>
<p>Looking around, I could not find another frontmatter library for Go. I know that
python has a decent library which supports writing back to it (I used it in the
<a href="https://icle.es/wordsonsand/despatches.md">depatcher</a> but I don&rsquo;t want to write python.</p>
<h2 id="multiple-formats">Multiple Formats</h2>
<p>Let&rsquo;s make it extendable by defining a <code>Format</code> that will allow us to add in
other formats later:</p>
```go
// A Format knows how to (un)marshal a particular format of frontmatter.
// e.g. yaml, toml etc.
type Format struct {
	// TODO: We could add details like delimiter to support other formats
	// and auto detection of format
	Unmarshal UnmarshalFunc
	Marshal   MarshalFunc
}

var yamlFormat = Format{
	Unmarshal: yaml.Unmarshal,
	Marshal:   yaml.Marshal,
}
```
<h2 id="writing-back">Writing Back</h2>
<p>We could also do with a struct to hold the whole file contents so that we can
write it back easier.</p>
```go
// A Scribed is a representation of a file that contains frontmatter and markdown content
type Scribed struct {
	format      Format
	frontmatter []byte
	Content     string
}
```
<p>By storing the full frontmatter, we can later accept partial updates without
losing other keys.</p>
<h2 id="naive-merging-of-updates">Naive Merging of Updates</h2>
<p>We (the user) should be able to update just the keys we care about. All the
other keys should be preserved.</p>
<p>The easiest way I could find to do this was to Marshal, Unmarshall and then
merge with the raw Unmarshall:</p>
<h3 id="minimal-merge-strategy-loses-order-and-formatting">Minimal merge strategy (loses order and formatting)</h3>
```go
// Merge frontmatter
var raw map[string]any // full unmarshalled frontmatter
err := s.format.Unmarshal(s.frontmatter, &raw)

updatedBytes, _ := yaml.Marshal(fm) // convert updated to yaml

var updates map[string]any
yaml.Unmarshal(updatedBytes, &updates) // get updated keys as map

for k, v := range updates {
    raw[k] = v // overwrite only touched fields
}

// raw is now the preserved + updated keys
data, err := s.format.Marshal(raw)
if err != nil {
    return err
}
```
<blockquote>
<p>⚠️ <strong>Warning</strong>: Key ordering is lost</p>
<p>Due to the way maps work, the key ordering is lost More accurately, the keys
are sorted alphabetically during write.</p>
<p>It fully rewrites the frontmatter, which also means that double quotes might
disappear etc.</p></blockquote>
<h2 id="in-place-merging-of-updates">In Place Merging of Updates</h2>
<p>If it is important to keep the frontmatter formatting as much as possible, we
need to bigger sledgehammer.</p>
<p>I explored an approach using yaml.Node with ChatGPT&rsquo;s help.</p>
<p>I fitted it into the <code>Format</code> as well</p>
```go
type MergeFunc func(raw []byte, fm any) ([]byte, error)

type Format struct {
	// TODO: We could add details like delimiter to support other formats
	// and auto detection of format
	Unmarshal UnmarshalFunc
	Marshal   MarshalFunc
	Merge     MergeFunc
}

func MergeYaml(raw []byte, fm any) ([]byte, error) {
	var node yaml.Node
	if err := yaml.Unmarshal(raw, &node); err != nil {
		return nil, err
	}

	// Expecting a document with a single mapping node
	if len(node.Content) == 0 || node.Content[0].Kind != yaml.MappingNode {
		return nil, errors.New("invalid frontmatter")
	}

	b, err := yaml.Marshal(fm)
	if err != nil {
		return nil, err
	}

	var updates map[string]string
	yaml.Unmarshal(b, &updates)

	m := node.Content[0]
	for key, value := range updates {
		// Search for the key and update it, or append a new one
		found := false
		for i := 0; i < len(m.Content); i += 2 {
			k := m.Content[i]
			if k.Value == key {
				m.Content[i+1].Value = value
				found = true
				break
			}
		}
		if !found {
			m.Content = append(m.Content,
				&yaml.Node{Kind: yaml.ScalarNode, Value: key},
				&yaml.Node{Kind: yaml.ScalarNode, Value: value},
			)
		}
	}

	var buf bytes.Buffer
	enc := yaml.NewEncoder(&buf)
	if err := enc.Encode(&node); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}
```
<p>This preserves frontmatter formatting well - but it only handles flat YAML.
Nested maps require a bit more work.</p>
<h2 id="supporting-maps-etc-as-values">Supporting maps etc. as values</h2>
<p>We need to switch to <code>map[string]any</code> and tweak a bit of the loop</p>
```go
var updates map[string]any
yaml.Unmarshal(b, &updates)

m := node.Content[0]
for key, value := range updates {
    // Encode value to a yaml.Node
    valNode := &yaml.Node{}
    if err := valNode.Encode(value); err != nil {
        return nil, err
    }

    // Search and replace or append
    found := false
    for i := 0; i < len(m.Content); i += 2 {
        if m.Content[i].Value == key {
            m.Content[i+1] = valNode
            found = true
            break
        }
    }
    if !found {
        m.Content = append(m.Content,
            &yaml.Node{Kind: yaml.ScalarNode, Value: key},
            valNode,
        )
    }
}
```
<p>It&rsquo;s funny how things can be more complicated than it first seems.</p>
<h2 id="minor-tweaks">Minor tweaks</h2>
<p>I also wanted to add the delimiter into the format and use that instead of
hardcoding, which was easy enough.</p>
```go
// A Format knows how to (un)marshal a particular format of frontmatter.
// e.g. yaml, toml etc.
type Format struct {
	Delimiter string
	Unmarshal UnmarshalFunc
	Marshal   MarshalFunc
	Merge     MergeFunc
}

var yamlFormat = Format{
	Delimiter: "---",
	Unmarshal: yaml.Unmarshal,
	Marshal:   yaml.Marshal,
	Merge:     MergeYaml,
}

func (s *Scribed) Write(fm any, out io.Writer) error {
	// Merge frontmatter
	data, err := s.format.Merge(s.frontmatter, fm)
	if err != nil {
		return err
	}

	io.WriteString(out, s.format.Delimiter+"\n")
	out.Write(data)
	io.WriteString(out, s.format.Delimiter+"\n\n")
	io.WriteString(out, s.Content)

	return nil
}

// splitFrontmatter will split frontmatter from Content and store them
func (s *Scribed) splitFrontmatter(r io.Reader) error {
	data, err := io.ReadAll(r)
	if err != nil {
		return err
	}

	parts := bytes.SplitN(data, []byte("\n"+s.format.Delimiter+"\n"), 2)
	if len(parts) != 2 {
		return errors.New("invalid frontmatter format")
	}

	// Remove the opening '---\n' from the first part
	s.frontmatter = bytes.TrimPrefix(parts[0], []byte("---\n"))

	s.Content = string(bytes.TrimLeft(parts[1], "\r\n"))

	return nil
}
```
<h2 id="conclusion">Conclusion</h2>
<p>This is probably the main bits of functionality I&rsquo;ll need to continue with the
<a href="https://icle.es/wordsonsand/projector-sync.md">projector sync</a>.</p>
<p>I had considered <code>frontmatter</code> to be a blackbox with complicated functionality,
but cutting it up and working on it has demystified it and made it easier to
work with. ChatGPT helped.</p>
<p>It should be fairly straightforward to add other frontmatter formats like TOML,
and to autodetect the formats, but I don&rsquo;t need it right now.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://github.com/drone-ah/wordsonsand/tree/main/lib/inscribe">Source Code on GitHub</a></li>
</ul>
]]></content:encoded></item><item><title>SVG To TVG</title><link>https://icle.es/2025/06/07/svg-to-tvg/</link><pubDate>Sat, 07 Jun 2025 17:02:22 +0000</pubDate><guid>https://icle.es/2025/06/07/svg-to-tvg/</guid><description>&lt;p>I am using &lt;a href="https://github.com/david-vanderson/dvui">dvui&lt;/a> in a project and it
uses &lt;a href="https://tinyvg.tech/">TinyVG&lt;/a> for its icon format.&lt;/p>
&lt;p>While it sounds swell, most icons I could find were still
&lt;a href="https://en.wikipedia.org/wiki/SVG">SVG&lt;/a>.&lt;/p>
&lt;p>When I tried to use a standard SVG, I got the following errors:&lt;/p>
```
warning(dvui): iconTexture Tinyvg error error.InvalidData rendering icon craft at height 16

warning(dvui): iconWidth Tinyvg error error.InvalidData parsing icon craft
```</description><content:encoded><![CDATA[<p>I am using <a href="https://github.com/david-vanderson/dvui">dvui</a> in a project and it
uses <a href="https://tinyvg.tech/">TinyVG</a> for its icon format.</p>
<p>While it sounds swell, most icons I could find were still
<a href="https://en.wikipedia.org/wiki/SVG">SVG</a>.</p>
<p>When I tried to use a standard SVG, I got the following errors:</p>
```
warning(dvui): iconTexture Tinyvg error error.InvalidData rendering icon craft at height 16

warning(dvui): iconWidth Tinyvg error error.InvalidData parsing icon craft
```
<p>I couldn&rsquo;t find a lot of resources online on how to convert an SVG file into
TinyVG.</p>
<p>On their <a href="https://tinyvg.tech/">website, under tooling</a>, there are links with
binaries for various operating systems.</p>
<p>I am not a fan of downloading binaries, but it seemed reasonable - maybe run it
in a <a href="https://wiki.archlinux.org/title/Chroot">chroot</a> or container to be safe.</p>
<p>It needed two steps:</p>
<h2 id="convert-from-svg-to-tinyvg-text">Convert from SVG to TinyVG Text</h2>
```bash
./svg2tvgt <path-to-svg-file>
```
<p>It will put the output in the same directory as the source file</p>
<h2 id="convert-from-tinyvg-text-to-binary">Convert from TinyVG Text to Binary</h2>
```bash
./tvg-text -I tvgt -O tvg <path-to-tvgt-file>
```
<p>It will output the binary tvg file in the same directory as the source</p>
<h2 id="repo--status">Repo &amp; Status</h2>
<p>The <a href="https://github.com/TinyVG/sdk">SDK Repo</a> is using Zig 0.11 and hasn&rsquo;t had
an update in almost a year. There is a PR pending to update it to Zig 0.14,
which has been waiting a month.</p>
<p>It doesn&rsquo;t build on Zig 0.14 as it stands.</p>
<p>I worry that it might already be abandoned, but am hopeful that it will come
back to life.</p>]]></content:encoded></item></channel></rss>