Module: TypstRails::Helpers

Included in:
Renderer
Defined in:
lib/typst_rails/helpers.rb

Overview

Helper methods for working with Typst templates.

This module provides utilities for:

  • Escaping special Typst characters
  • Converting HTML to Markdown and Typst
  • Converting Markdown to Typst syntax
  • Including external Markdown files
  • Sanitizing HTML for security
  • URL encoding

These helpers are included in the Renderer and available in Rails ERB templates when using the Typst template handler.

Examples:

Using in Rails ERB template

<%# app/views/reports/monthly.typ.erb %>
<% data = { title: escape_typst(@title) } %>
= #data.title

Using standalone

include TypstRails::Helpers
safe_text = escape_typst("Price: $100")
# => "Price: \\$100"

Constant Summary collapse

DEFAULT_ALLOWED_TAGS =

MARK: - HTML Sanitization

%w[
  h1 h2 h3 h4 h5 h6 p br strong em u s del ins
  ul ol li blockquote pre code a img table thead
  tbody tr th td
].freeze
DEFAULT_ALLOWED_ATTRIBUTES =
%w[href src alt title].freeze

Instance Method Summary collapse

Instance Method Details

#escape_typst(text) ⇒ String

MARK: - Text Escaping Escapes text for safe use in Typst documents.

Escapes special Typst characters that have syntactic meaning:

  • # (code/scripting)
  • $ (math mode)
  • * (emphasis)
  • _ (emphasis)
  • [, ] (content blocks)
  • \ (escape character)
  • <, > (labels and references)
  • {`, `} (code blocks)
  • @ (references)

Examples:

Escaping special characters

escape_typst("Hello #world")
# => "Hello \\#world"

Escaping currency

escape_typst("Price: $100")
# => "Price: \\$100"

Handling nil

escape_typst(nil)
# => ""

Parameters:

  • text (String, nil)

    The text to escape

Returns:

  • (String)

    The escaped text, or empty string if text is nil

Raises:

  • (ArgumentError)

    if text is not a String or nil

See Also:



62
63
64
65
66
67
68
69
# File 'lib/typst_rails/helpers.rb', line 62

def escape_typst(text)
  return "" if text.nil?
  raise ArgumentError, "text must be a String" unless text.is_a?(String)

  # Escape special Typst characters
  # See: https://typst.app/docs/reference/syntax/
  text.gsub(/([#\$*_\[\]\\<>{}@])/, '\\\\\1')
end

#html_to_markdown(html, options = {}) ⇒ String

Converts HTML to Markdown for use in Typst documents.

Typst has excellent support for Markdown syntax, making this a convenient way to include HTML content in Typst documents. The conversion uses the ReverseMarkdown library.

Examples:

Converting headings and text

html_to_markdown("<h1>Title</h1><p>Content</p>")
# => "# Title\n\nContent"

Converting formatted text

html_to_markdown("<strong>Bold</strong> text")
# => "**Bold** text"

With options

html_to_markdown("<p>Text</p>", unknown_tags: :bypass)

Parameters:

  • html (String, nil)

    The HTML to convert

  • options (Hash) (defaults to: {})

    Options to pass to ReverseMarkdown

Returns:

  • (String)

    The converted Markdown, or empty string if html is nil

Raises:

  • (ArgumentError)

    if html is not a String or nil

  • (ArgumentError)

    if options is not a Hash

  • (TypstRails::Error)

    if conversion fails

See Also:



98
99
100
101
102
103
104
105
106
107
108
# File 'lib/typst_rails/helpers.rb', line 98

def html_to_markdown(html, options = {})
  return "" if html.nil?
  raise ArgumentError, "html must be a String" unless html.is_a?(String)
  raise ArgumentError, "options must be a Hash" unless options.is_a?(Hash)

  begin
    ReverseMarkdown.convert(html, options)
  rescue StandardError => e
    raise Error, "Failed to convert HTML to Markdown: #{e.message}"
  end
end

#html_to_typst(html, options = {}) ⇒ String

Converts HTML to Typst-compatible markup.

This is a convenience method that combines html_to_markdown with markdown_to_typst to convert HTML directly to Typst syntax.

Examples:

Converting HTML headings to Typst

html_to_typst("<h1>Title</h1>")
# => "= Title\n\n"

Converting formatted HTML

html_to_typst("<strong>Bold</strong> and <em>italic</em>")
# => "*Bold* and _italic_"

Parameters:

  • html (String, nil)

    The HTML to convert

  • options (Hash) (defaults to: {})

    Options to pass to the HTML converter

Returns:

  • (String)

    Typst-compatible markup

Raises:

  • (ArgumentError)

    if html is not a String or nil

  • (ArgumentError)

    if options is not a Hash

  • (TypstRails::Error)

    if conversion fails

See Also:



132
133
134
135
# File 'lib/typst_rails/helpers.rb', line 132

def html_to_typst(html, options = {})
  markdown = html_to_markdown(html, options)
  markdown_to_typst(markdown)
end

#include_markdown(markdown_path) ⇒ String

Note:

File path is relative to the current working directory

Reads and includes Markdown content, converting it to Typst syntax.

This method is useful for including external Markdown files in Typst documents. The file is read and converted to Typst syntax.

Examples:

Including a content file

include_markdown("./content.md")
# Returns the converted content of content.md

Using in a Typst template

# In your .typ.erb template:
<%= include_markdown("sections/introduction.md") %>

Parameters:

  • markdown_path (String)

    Path to the Markdown file (relative or absolute)

Returns:

  • (String)

    Typst-compatible content from the file

Raises:



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/typst_rails/helpers.rb', line 234

def include_markdown(markdown_path)
  raise ArgumentError, "markdown_path must be a String" unless markdown_path.is_a?(String)
  raise ArgumentError, "markdown_path cannot be empty" if markdown_path.empty?

  begin
    markdown_content = File.read(markdown_path)
    markdown_to_typst(markdown_content)
  rescue Errno::ENOENT
    raise Error, "Markdown file not found: #{markdown_path}"
  rescue Errno::EACCES
    raise Error, "Permission denied reading Markdown file: #{markdown_path}"
  rescue StandardError => e
    raise Error, "Failed to read Markdown file #{markdown_path}: #{e.message}"
  end
end

#markdown_to_typst(markdown) ⇒ String

Converts Markdown to Typst syntax.

Handles common Markdown patterns and converts them to Typst equivalents:

  • Headings (#=)
  • Bold (**text***text*)
  • Italic (*text*_text_)
  • Links ([text](url)#link("url")[text])
  • Images (![alt](url)#image("url"))

Examples:

Converting headings

markdown_to_typst("# Title")
# => "= Title"

Converting subheadings

markdown_to_typst("## Subtitle")
# => "== Subtitle"

Converting formatted text

markdown_to_typst("**bold** and *italic*")
# => "*bold* and _italic_"

Converting links

markdown_to_typst("[Typst](https://typst.app)")
# => "#link(\"https://typst.app\")[Typst]"

Parameters:

  • markdown (String, nil)

    The Markdown text to convert

Returns:

  • (String)

    Typst-compatible markup, or empty string if markdown is nil

Raises:

  • (ArgumentError)

    if markdown is not a String or nil

See Also:



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/typst_rails/helpers.rb', line 169

def markdown_to_typst(markdown)
  return "" if markdown.nil?
  raise ArgumentError, "markdown must be a String" unless markdown.is_a?(String)

  result = markdown.dup

  # Convert Markdown headers to Typst headers
  # # Title -> = Title
  # ## Subtitle -> == Subtitle
  # etc.
  result.gsub!(/^(#{Regexp.quote("#")}{1,6})\s+(.+)$/) do
    "#{"=" * Regexp.last_match(1).length} #{Regexp.last_match(2)}"
  end

  # Convert Markdown bold to Typst bold, using placeholder bytes (\x01 text \x02)
  # so the italic pass below doesn't re-convert the resulting *text* markers.
  # **text** or __text__ -> *text*
  result.gsub!(/(\*\*|__)(.+?)\1/, "\x01\\2\x02")

  # Convert Markdown italic to Typst italic (underscore style)
  # *text* or _text_ -> _text_
  result.gsub!(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/, '_\1_')
  result.gsub!(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/, '_\1_')

  # Replace bold placeholders with Typst bold markers
  result.gsub!("\x01", "*")
  result.gsub!("\x02", "*")

  # Convert Markdown code to Typst code
  # `code` -> `code` (same in Typst)

  # Convert Markdown images (must run before links, since ![alt](url)
  # would otherwise be matched by the link pattern below)
  # ![alt](url) -> #image("url")
  result.gsub!(/!\[([^\]]*)\]\(([^)]+)\)/, '#image("\2")')

  # Convert Markdown links
  # [text](url) -> #link("url")[text]
  result.gsub!(/\[([^\]]+)\]\(([^)]+)\)/, '#link("\2")[\1]')

  result
end

#sanitize_html(html, allowed_tags: nil, allowed_attributes: nil) ⇒ String

Note:

This is basic sanitization. For production use with untrusted HTML, consider using a dedicated sanitization library like Loofah or Sanitize

Sanitizes HTML before conversion to prevent XSS attacks.

This method removes potentially dangerous tags and attributes from HTML before conversion to Typst. It provides basic XSS protection by:

  • Removing <script> and <style> tags
  • Removing event handler attributes (onclick, onload, etc.)
  • Optionally filtering to allowed tags and attributes

Examples:

Basic sanitization

sanitize_html("<script>alert('xss')</script><p>Safe content</p>")
# => "<p>Safe content</p>"

Removing event handlers

sanitize_html('<div onclick="evil()">Click</div>')
# => "<div>Click</div>"

Custom allowed tags

sanitize_html("<p>Text</p><img src='x'>", allowed_tags: %w[p])
# => "<p>Text</p>"

Parameters:

  • html (String, nil)

    The HTML to sanitize

  • allowed_tags (Array<String>, nil) (defaults to: nil)

    List of allowed HTML tags (uses defaults if nil)

  • allowed_attributes (Array<String>, nil) (defaults to: nil)

    List of allowed attributes (uses defaults if nil)

Returns:

  • (String)

    Sanitized HTML, or empty string if html is nil

Raises:

  • (ArgumentError)

    if html is not a String or nil



288
289
290
291
292
293
294
295
296
297
298
# File 'lib/typst_rails/helpers.rb', line 288

def sanitize_html(html, allowed_tags: nil, allowed_attributes: nil)
  return "" if html.nil?
  raise ArgumentError, "html must be a String" unless html.is_a?(String)

  allowed_tags ||= DEFAULT_ALLOWED_TAGS
  allowed_attributes ||= DEFAULT_ALLOWED_ATTRIBUTES

  fragment = Nokogiri::HTML5.fragment(html)
  strip_disallowed_nodes(fragment, allowed_tags, allowed_attributes)
  fragment.to_html
end

#url_encode(text) ⇒ String

URL-encodes text for safe use in links.

Uses CGI.escape to encode special characters for URL safety. Useful when constructing URLs or query parameters in Typst templates.

Examples:

Encoding a search query

url_encode("hello world")
# => "hello+world"

Encoding special characters

url_encode("hello & goodbye")
# => "hello+%26+goodbye"

Building a URL

query = url_encode(user_input)
url = "https://example.com/search?q=#{query}"

Parameters:

  • text (String, nil)

    The text to encode

Returns:

  • (String)

    URL-encoded text, or empty string if text is nil

Raises:

  • (ArgumentError)

    if text is not a String or nil

See Also:

  • CGI.escape


347
348
349
350
351
352
# File 'lib/typst_rails/helpers.rb', line 347

def url_encode(text)
  return "" if text.nil?
  raise ArgumentError, "text must be a String" unless text.is_a?(String)

  CGI.escape(text)
end