summaryrefslogtreecommitdiffhomepage
path: root/gleam/src/ocda_write.gleam
blob: 3c224fa570d4838991304b137c091c1f05e42f23 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//// ocda-write: a sample writer over the abstraction.
////
////   ocda-write --text|--html <file>.ssp|<file>.ocda.db|<file>.zip [lang]
////
//// The input may be either serialisation, and the writer is not told which.
//// That is the claim being demonstrated: a writer is a function from the
//// model to bytes, so a document renders the same whichever of its two
//// abstraction files it is given.
////
//// Output goes to stdout, so that the comparison against spine's own output
//// is a shell pipeline and not a directory of files to clean up.

import argv
import gleam/list
import gleam/string
import ocda/images
import gleam/io
import ocda/load
import simplifile
import write/epub
import write/html
import write/markdown
import write/text
import write/typst

pub fn main() {
  case argv.load().arguments {
    ["--text", path] -> render(path, "", "text")
    ["--text", path, lang] -> render(path, lang, "text")
    ["--html", path] -> render(path, "", "html")
    ["--html", path, lang] -> render(path, lang, "html")
    ["--epub", path, "--out", out] -> write_epub(path, "", out)
    ["--epub", path, lang, "--out", out] -> write_epub(path, lang, out)
    ["--md", path] -> render_markdown(path, "", "image")
    ["--md", path, lang] -> render_markdown(path, lang, "image")
    ["--md", path, "--image-prefix", pth] -> render_markdown(path, "", pth)
    ["--md", path, lang, "--image-prefix", pth] ->
      render_markdown(path, lang, pth)
    ["--typ", path] -> render(path, "", "typst")
    ["--typ", path, lang] -> render(path, lang, "typst")
    ["--typst", path] -> render(path, "", "typst")
    ["--typst", path, lang] -> render(path, lang, "typst")
    ["--typst", path, "--out", out] -> write_typst(path, "", out)
    ["--typst", path, lang, "--out", out] -> write_typst(path, lang, out)
    _ -> usage()
  }
}

fn usage() -> Nil {
  io.println_error(
    "usage: ocda-write --text|--html|--typst <file> [lang]\n"
      <> "       ocda-write --md <file> [lang] [--image-prefix <path>]\n"
      <> "       ocda-write --epub <file> [lang] --out <file>.epub",
  )
  halt(2)
}

fn render(path: String, lang: String, format: String) -> Nil {
  case load.load(path, lang, False) {
    Error(message) -> {
      io.println_error("error: " <> message)
      halt(2)
    }
    Ok(doc) ->
      io.print(case format {
        "html" -> html.write(doc)
        "typst" -> typst.write(doc)
        _ -> text.write(doc)
      })
  }
}

/// Markdown takes one thing the other formats do not: where the images are,
/// relative to wherever the .md is put. It has its own entry for that, as the
/// epub and the typst do for --out.
fn render_markdown(path: String, lang: String, image_pth: String) -> Nil {
  case load.load(path, lang, False) {
    Error(message) -> {
      io.println_error("error: " <> message)
      halt(2)
    }
    Ok(doc) -> io.print(markdown.write(doc, image_pth))
  }
}

/// An epub is a zip, so it goes to a file rather than to stdout.
fn write_epub(path: String, lang: String, out: String) -> Nil {
  case load.load(path, lang, False) {
    Error(message) -> {
      io.println_error("error: " <> message)
      halt(2)
    }
    Ok(doc) ->
      case simplifile.write_bits(out, epub.write(doc, path)) {
        Ok(_) -> Nil
        Error(e) -> {
          io.println_error("error: " <> out <> ": " <> simplifile.describe_error(e))
          halt(2)
        }
      }
  }
}

/// Gleam has no standard exit, and the exit status is what a sweep script
/// reads.
@external(erlang, "erlang", "halt")
fn halt(code: Int) -> Nil

/// A .typ names its images by path rather than carrying them, so when it goes
/// to a file the images are written beside it.
fn write_typst(path: String, lang: String, out: String) -> Nil {
  case load.load(path, lang, False) {
    Error(message) -> {
      io.println_error("error: " <> message)
      halt(2)
    }
    Ok(doc) -> {
      case simplifile.write(out, typst.write(doc)) {
        Ok(_) -> Nil
        Error(e) -> {
          io.println_error(
            "error: " <> out <> ": " <> simplifile.describe_error(e),
          )
          halt(2)
        }
      }
      let dir = case string.split(out, "/") {
        [_] -> "image"
        parts ->
          string.join(list.take(parts, list.length(parts) - 1), "/") <> "/image"
      }
      case images.can_give_images(path) {
        False -> Nil
        True ->
          epub.images_named(doc)
          |> list.each(fn(name) {
            case images.image_bytes(path, name) {
              Ok(bits) -> {
                let _ = simplifile.create_directory_all(dir)
                let _ = simplifile.write_bits(dir <> "/" <> name, bits)
                Nil
              }
              Error(_) -> Nil
            }
          })
      }
    }
  }
}