Using Typst In Elixir

Using Typst In Elixir

Typst is an application which is used for creating pdf and images from typst files. Typst has a custom templating syntax which it uses for constructing various page elements and it also has a scripting system much like a programming language which enables you to do some really cool stuff.

It’s written in rust which makes it quite performant and gives it some type safe guarantees.it has a packaging system which enables you to import other packages written by others into your typst project.it is also multiplatform as well.its similar to latex in what it does and its functionality.

First of all let’s create a new phoenix project to show its usage.This project will be a blog where we will have the ability to create,read,update,preview,download and delete blog posts.

First we create a new phoenix project

1
	mix phx.new typst_app

After creating the project,we need to have the typst executable in the priv folder of our phoenix app.We go to the typst github page and follow the instructions for installation.

After installation of typst, we get the executable and then place it in the priv folder.I usually create a priv/typst folder and put the executable there.We also create two typst files and put them in the folder also.

One is an api file(main.typ) and the other a template file(blog.typ) which contains the blog typst code.Seperation of the api file and the template typst files enables the template to be used in other scenarios and enables seperation of concern also.

main.typ

1
2
3
4
5
#import sys.inputs.templatePath: blog-from-metadata


#let meta = json.decode(sys.inputs.blogJson)
#blog-from-metadata(meta, apply-default-style: true)

blog.typ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#let format-session-matter(blog-info) = [
  = Blog Details
  #line(length: 100%)
  #v(1em) 
  *Id*: #blog-info.id\

  *Title*: #blog-info.title\	

   *Body*:
  
   #blog-info.body\
  
]

#let blog-from-metadata(metadata-dict, ..extra-blog-args) = {
  let meta = metadata-dict
 
  //contains  code for displaying blog information
  let sessionmatter = format-session-matter(meta)

  sessionmatter

 }

We then run the generator for live view to generate some scaffolding and boilerplate code

1
2
	cd typst_app
	mix phx.gen.live Blog Post posts title:string body:text

We are now going to add some endpoints for the preview/downloading and also for the preview modal. We add the following endpoints th the router.ex.

1
2
    get "/process_blog/:id/:type", PageController, :process_blog
    live "/posts/:id/preview", PostLive.Index, :preview

the first link enables you to process a blog based on the id and the type.the type can be either view or download and enables you to view and download the pdf respectivity. the second link enables you to preview the pdf in a modal.

We create an action in TypstAppWeb.PostLive.Index to process the preview action.

1
2
3
4
5
  defp apply_action(socket, :preview, %{"id" => id}) do
    socket
    |> assign(:page_title, "Preview Post Pdf")
    |> assign(:post, Blog.get_post!(id))
  end

We add a function process_blog in TypstAppWeb.PageController to enable us to get the blog information,encode to json which is acceptable to typst,shell out and execute the typst command for creating the pdf/png output file and send the generated file to the client.In my case i used the png for the preview and the pdf for the download option.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  def process_blog(conn,%{"id" => id,"type" => type}) do
    data = Blog.get_post!(id)
    jason_map = %{"id" => data.id,"title" => data.title, "body" => data.body}
    {:ok,blog_data} = Jason.encode(jason_map) 
    {disposition,format} = 
      case type do
	  "view" -> {:inline,"png"}
	  "download" -> {:attachment,"pdf"}
      end
    filename = "Blog #{id}"
    path_typst = Path.join([:code.priv_dir(:typst_app),"/typst/typst"] ) 
    path_api = Path.join([:code.priv_dir(:typst_app),"/typst/main.typ"] )
    path_template = "blog.typ"
    command = "#{path_typst} compile  --input 'blogJson=#{blog_data}' 
	--input 'templatePath=#{path_template}' -f #{format} #{path_api} -"
    {result,code_result} = System.cmd("sh",["-c",command])
    case code_result do
      0 -> send_download(conn,{:binary,result},content_type: "application/pdf",
	       disposition: disposition,filename: filename )
      _ -> text(conn,"error generating pdf")
    end
    
  end

We edit the index.html.heex file and add links after the edit link for previewing as well as downloading the blog as well.one is a patch and one is a normal href link. .

1
2
3
4
5
6
<:action :let={{_id, post}}>
     <.link patch={~p"/posts/#{post}/preview"}>Preview</.link> 
  </:action>    
  <:action :let={{_id, post}}>
     <.link href={~p"/process_blog/#{post}/download"}>Download</.link>
  </:action> 

We also add a modal component to index.html.heex to enable you to create a preview modal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<.modal :if={@live_action in [:preview]}  id="preview-modal" show 
  on_cancel={JS.patch(~p"/posts")}>
  <div>
    <.header>
      {@page_title}
      <:subtitle>Use this form to preview blog # {@post.id}.</:subtitle>
    </.header>
    <img class="flex items-left justify-left" 
	src={~p"/process_blog/#{@post.id}/view"}/>
  </div>
</.modal>

Thats it.This has been interesting.

Typst is an easy and useful tool for generating pdfs in elixir apps. I imagine it being used in conjuction with oban for generating receipts,invoices and other attachments. So have fun with it and you can also find the link to this example repo here also.


Read more