Tome

Artifact Content
Login

Artifact 966e7d983bd7174e2505b9f693fe93e1a4a794de94addcbaad710e24a7088c87:


// Copyright (C) David N. Cohen

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.

// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package main

import (
	"errors"
	"flag"
	"fmt"
	"io"
	"log"
	"os"

	"golang.org/x/text/transform"
	"src.d10.dev/tome/internal/tome"
	"src.d10.dev/tome/typst"
)

func init() {
	cmd.Operation("typst",
		"typst [-suffix=.typ] [DIRECTORY ...]",
		"writes notes to file in typst format",
		opTypst,
	)
}

func opTypst() (err error) {
	defer func() {
		if err != nil {
			log.Printf("opTypst failed: %v", err)
		}
	}()

	md := flag.Bool("md", true, "convert markdown headers into typst headers")
	outfile := flag.String("o", "", "filename where to write typst markup")

	flag.Parse()

	if len(Suffix) == 0 {
		Suffix = []string{".typ"}
		if *md {
			Suffix = append(Suffix, ".md")
		}
	}

	arg := flag.Args()
	if len(arg) == 0 {
		wd, err := os.Getwd()
		if err != nil {
			return err
		}

		arg = append(arg, wd) // default find content in current directory
	}

	filenames := []string{}
	for i := range arg {
		names, err := tome.FindSuffix(arg[i], Suffix...)
		if err != nil {
			return fmt.Errorf("failed to find content in directory (%q): %w", arg[i], err)
		}
		sortAlphaWithFirst(names, "README.*")

		// overall list preserves directory order of args
		filenames = append(filenames, names...)
	}

	out := os.Stdout
	log.Printf("outfile (%q)", *outfile)
	if *outfile != "" {
		out, err = os.OpenFile(*outfile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
		if err != nil {
			return fmt.Errorf("cannot open output file (%q): %w", *outfile, err)
		}
		defer func() {
			err = errors.Join(err, out.Close())
		}()
	}

	// one-time definitions at top of typ file
	_, err = fmt.Fprint(out, header)
	if err != nil {
		return fmt.Errorf("failed to write typst header: %w", err)
	}

	for _, name := range filenames {
		f, err := os.Open(name)
		if err != nil {
			return err
		}
		log.Printf("transforming file %q", name)

		var xform []transform.Transformer
		if *md {
			xform = append(xform, typst.NewMd(name)...)
		}
		// last
		xform = append(xform, typst.New(name))

		// avoid transform.Chain because it has a fixed dst buffer size, so we'd fail with ErrShortDst
		// actually problem with chain was probably misunderstanding of how ErrShortDst works, it probably would work as intended

		// var pipeline io.Writer = out
		// for _, x := range xform {
		// 	tw := transform.NewWriter(pipeline, x)
		// 	defer tw.Close()
		// 	pipeline = tw
		// }

		pipeline := transform.NewWriter(out, transform.Chain(xform...))
		defer pipeline.Close()

		_, err = io.Copy(pipeline, f)
		if err != nil {
			return err
		}
	}

	return nil
}

const header = `
// style
#show link: underline
#show link: set text(blue)

// prevent broken links from breaking compile
#show link: it => {
  if type(it.dest) != label {
    return it
  }
  if query(it.dest).len() > 0 {
    it
  } else {
    text(fill: red)[#it.body]
  }
}

`