typst-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +68 -0
- data/MIT-LICENSE +20 -0
- data/README.md +264 -0
- data/lib/tasks/typst_rails/tasks.rake +6 -0
- data/lib/typst_rails/backends/base.rb +30 -0
- data/lib/typst_rails/backends/cli.rb +90 -0
- data/lib/typst_rails/backends/gem.rb +43 -0
- data/lib/typst_rails/backends/registry.rb +82 -0
- data/lib/typst_rails/backends.rb +15 -0
- data/lib/typst_rails/framework_detection.rb +26 -0
- data/lib/typst_rails/handler.rb +75 -0
- data/lib/typst_rails/helpers.rb +354 -0
- data/lib/typst_rails/rage_integration.rb +18 -0
- data/lib/typst_rails/railtie.rb +18 -0
- data/lib/typst_rails/renderer.rb +386 -0
- data/lib/typst_rails/sinatra_integration.rb +33 -0
- data/lib/typst_rails/version.rb +5 -0
- data/lib/typst_rails.rb +146 -0
- metadata +225 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "typst_rails/renderer" # Loads the Renderer class
|
|
5
|
+
|
|
6
|
+
module TypstRails
|
|
7
|
+
# Template handler for Typst (.typ) files.
|
|
8
|
+
# This handler allows ERB preprocessing of Typst templates, enabling dynamic content
|
|
9
|
+
# within Typst source files using standard Rails view instance variables and helpers.
|
|
10
|
+
# The ERB-processed Typst source is then passed to the `TypstRails::Renderer`
|
|
11
|
+
# for compilation into a PDF.
|
|
12
|
+
class Handler
|
|
13
|
+
# This method is called by ActionView to compile the template.
|
|
14
|
+
# It returns a string of Ruby code that, when evaluated, will render the template.
|
|
15
|
+
#
|
|
16
|
+
# @param template [ActionView::Template] The template object, providing access to
|
|
17
|
+
# metadata like `source` (the raw template content) and `identifier`.
|
|
18
|
+
# @param source [String, nil] The raw template source code. If nil, `template.source` is used.
|
|
19
|
+
# ActionView typically passes the source content as this second argument.
|
|
20
|
+
# @return [String] A Ruby code string. This string will be evaluated by ActionView
|
|
21
|
+
# in the context of an `ActionView::Base` instance (the view context).
|
|
22
|
+
def self.call(template, source = nil)
|
|
23
|
+
actual_source = source || template.source
|
|
24
|
+
|
|
25
|
+
# Configure ERB to behave like Rails' default ERB handler.
|
|
26
|
+
# This involves setting the output buffer variable to '@output_buffer'
|
|
27
|
+
# and enabling standard trim mode for ERB tags like '<%-' and '-%>'.
|
|
28
|
+
erb_engine = ::ERB.new(
|
|
29
|
+
actual_source,
|
|
30
|
+
trim_mode: "-", # Standard Rails trim mode (e.g., <% foo -%> removes trailing newline)
|
|
31
|
+
eoutvar: "@output_buffer" # Specifies the output variable ERB should append to.
|
|
32
|
+
# This makes it compatible with ActionView's rendering flow.
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# `erb_engine.src` generates a string of Ruby code. This code, when executed,
|
|
36
|
+
# evaluates the ERB template and appends results to the `eoutvar` (i.e., `@output_buffer`).
|
|
37
|
+
erb_evaluation_code = erb_engine.src
|
|
38
|
+
|
|
39
|
+
# The string returned by this `call` method is the final piece of Ruby code
|
|
40
|
+
# that ActionView will `instance_eval`.
|
|
41
|
+
# This code performs two main steps:
|
|
42
|
+
# 1. Executes the `erb_evaluation_code` to process the Typst template through ERB,
|
|
43
|
+
# capturing the result. This allows dynamic Ruby evaluation within the .typ file.
|
|
44
|
+
# 2. Passes the ERB-processed Typst source to our `TypstRails::Renderer`
|
|
45
|
+
# which then handles the actual Typst compilation.
|
|
46
|
+
<<-RUBY_CODE
|
|
47
|
+
# Preserve the current @output_buffer if one exists (e.g., if rendering a partial).
|
|
48
|
+
# A new buffer is created for ERB processing of the Typst template to isolate its output.
|
|
49
|
+
_original_typst_handler_output_buffer = @output_buffer
|
|
50
|
+
@output_buffer = ActionView::OutputBuffer.new
|
|
51
|
+
|
|
52
|
+
#{erb_evaluation_code}
|
|
53
|
+
|
|
54
|
+
# After the above ERB code executes, @output_buffer contains the
|
|
55
|
+
# ERB-processed Typst template source as a string.
|
|
56
|
+
processed_typst_source = @output_buffer.to_s
|
|
57
|
+
|
|
58
|
+
# Restore the original output buffer that was in place before this handler ran.
|
|
59
|
+
@output_buffer = _original_typst_handler_output_buffer
|
|
60
|
+
|
|
61
|
+
typst_renderer = ::TypstRails::Renderer.new(processed_typst_source)
|
|
62
|
+
typst_renderer.render(self, local_assigns)
|
|
63
|
+
RUBY_CODE
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Indicates whether this template handler supports streaming.
|
|
67
|
+
# Typst compilation generates a complete PDF file; it's not inherently a streaming process
|
|
68
|
+
# where chunks of output can be sent progressively. Therefore, streaming is not supported.
|
|
69
|
+
#
|
|
70
|
+
# @return [Boolean] false, as Typst rendering is not streamable.
|
|
71
|
+
def self.supports_streaming?
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
require "reverse_markdown"
|
|
6
|
+
|
|
7
|
+
module TypstRails
|
|
8
|
+
# Helper methods for working with Typst templates.
|
|
9
|
+
#
|
|
10
|
+
# This module provides utilities for:
|
|
11
|
+
# - Escaping special Typst characters
|
|
12
|
+
# - Converting HTML to Markdown and Typst
|
|
13
|
+
# - Converting Markdown to Typst syntax
|
|
14
|
+
# - Including external Markdown files
|
|
15
|
+
# - Sanitizing HTML for security
|
|
16
|
+
# - URL encoding
|
|
17
|
+
#
|
|
18
|
+
# These helpers are included in the Renderer and available in Rails ERB templates
|
|
19
|
+
# when using the Typst template handler.
|
|
20
|
+
#
|
|
21
|
+
# @example Using in Rails ERB template
|
|
22
|
+
# <%# app/views/reports/monthly.typ.erb %>
|
|
23
|
+
# <% data = { title: escape_typst(@title) } %>
|
|
24
|
+
# = #data.title
|
|
25
|
+
#
|
|
26
|
+
# @example Using standalone
|
|
27
|
+
# include TypstRails::Helpers
|
|
28
|
+
# safe_text = escape_typst("Price: $100")
|
|
29
|
+
# # => "Price: \\$100"
|
|
30
|
+
module Helpers
|
|
31
|
+
# MARK: - Text Escaping
|
|
32
|
+
# Escapes text for safe use in Typst documents.
|
|
33
|
+
#
|
|
34
|
+
# Escapes special Typst characters that have syntactic meaning:
|
|
35
|
+
# - `#` (code/scripting)
|
|
36
|
+
# - `$` (math mode)
|
|
37
|
+
# - `*` (emphasis)
|
|
38
|
+
# - `_` (emphasis)
|
|
39
|
+
# - `[`, `]` (content blocks)
|
|
40
|
+
# - `\` (escape character)
|
|
41
|
+
# - `<`, `>` (labels and references)
|
|
42
|
+
# - `{`, `}` (code blocks)
|
|
43
|
+
# - `@` (references)
|
|
44
|
+
#
|
|
45
|
+
# @param text [String, nil] The text to escape
|
|
46
|
+
# @return [String] The escaped text, or empty string if text is nil
|
|
47
|
+
# @raise [ArgumentError] if text is not a String or nil
|
|
48
|
+
#
|
|
49
|
+
# @example Escaping special characters
|
|
50
|
+
# escape_typst("Hello #world")
|
|
51
|
+
# # => "Hello \\#world"
|
|
52
|
+
#
|
|
53
|
+
# @example Escaping currency
|
|
54
|
+
# escape_typst("Price: $100")
|
|
55
|
+
# # => "Price: \\$100"
|
|
56
|
+
#
|
|
57
|
+
# @example Handling nil
|
|
58
|
+
# escape_typst(nil)
|
|
59
|
+
# # => ""
|
|
60
|
+
#
|
|
61
|
+
# @see https://typst.app/docs/reference/syntax/ Typst syntax reference
|
|
62
|
+
def escape_typst(text)
|
|
63
|
+
return "" if text.nil?
|
|
64
|
+
raise ArgumentError, "text must be a String" unless text.is_a?(String)
|
|
65
|
+
|
|
66
|
+
# Escape special Typst characters
|
|
67
|
+
# See: https://typst.app/docs/reference/syntax/
|
|
68
|
+
text.gsub(/([#\$*_\[\]\\<>{}@])/, '\\\\\1')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# MARK: - HTML Conversion
|
|
72
|
+
|
|
73
|
+
# Converts HTML to Markdown for use in Typst documents.
|
|
74
|
+
#
|
|
75
|
+
# Typst has excellent support for Markdown syntax, making this a convenient
|
|
76
|
+
# way to include HTML content in Typst documents. The conversion uses the
|
|
77
|
+
# ReverseMarkdown library.
|
|
78
|
+
#
|
|
79
|
+
# @param html [String, nil] The HTML to convert
|
|
80
|
+
# @param options [Hash] Options to pass to ReverseMarkdown
|
|
81
|
+
# @return [String] The converted Markdown, or empty string if html is nil
|
|
82
|
+
# @raise [ArgumentError] if html is not a String or nil
|
|
83
|
+
# @raise [ArgumentError] if options is not a Hash
|
|
84
|
+
# @raise [TypstRails::Error] if conversion fails
|
|
85
|
+
#
|
|
86
|
+
# @example Converting headings and text
|
|
87
|
+
# html_to_markdown("<h1>Title</h1><p>Content</p>")
|
|
88
|
+
# # => "# Title\n\nContent"
|
|
89
|
+
#
|
|
90
|
+
# @example Converting formatted text
|
|
91
|
+
# html_to_markdown("<strong>Bold</strong> text")
|
|
92
|
+
# # => "**Bold** text"
|
|
93
|
+
#
|
|
94
|
+
# @example With options
|
|
95
|
+
# html_to_markdown("<p>Text</p>", unknown_tags: :bypass)
|
|
96
|
+
#
|
|
97
|
+
# @see https://github.com/xijo/reverse_markdown ReverseMarkdown documentation
|
|
98
|
+
def html_to_markdown(html, options = {})
|
|
99
|
+
return "" if html.nil?
|
|
100
|
+
raise ArgumentError, "html must be a String" unless html.is_a?(String)
|
|
101
|
+
raise ArgumentError, "options must be a Hash" unless options.is_a?(Hash)
|
|
102
|
+
|
|
103
|
+
begin
|
|
104
|
+
ReverseMarkdown.convert(html, options)
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
raise Error, "Failed to convert HTML to Markdown: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Converts HTML to Typst-compatible markup.
|
|
111
|
+
#
|
|
112
|
+
# This is a convenience method that combines html_to_markdown with
|
|
113
|
+
# markdown_to_typst to convert HTML directly to Typst syntax.
|
|
114
|
+
#
|
|
115
|
+
# @param html [String, nil] The HTML to convert
|
|
116
|
+
# @param options [Hash] Options to pass to the HTML converter
|
|
117
|
+
# @return [String] Typst-compatible markup
|
|
118
|
+
# @raise [ArgumentError] if html is not a String or nil
|
|
119
|
+
# @raise [ArgumentError] if options is not a Hash
|
|
120
|
+
# @raise [TypstRails::Error] if conversion fails
|
|
121
|
+
#
|
|
122
|
+
# @example Converting HTML headings to Typst
|
|
123
|
+
# html_to_typst("<h1>Title</h1>")
|
|
124
|
+
# # => "= Title\n\n"
|
|
125
|
+
#
|
|
126
|
+
# @example Converting formatted HTML
|
|
127
|
+
# html_to_typst("<strong>Bold</strong> and <em>italic</em>")
|
|
128
|
+
# # => "*Bold* and _italic_"
|
|
129
|
+
#
|
|
130
|
+
# @see #html_to_markdown
|
|
131
|
+
# @see #markdown_to_typst
|
|
132
|
+
def html_to_typst(html, options = {})
|
|
133
|
+
markdown = html_to_markdown(html, options)
|
|
134
|
+
markdown_to_typst(markdown)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# MARK: - Markdown Conversion
|
|
138
|
+
|
|
139
|
+
# Converts Markdown to Typst syntax.
|
|
140
|
+
#
|
|
141
|
+
# Handles common Markdown patterns and converts them to Typst equivalents:
|
|
142
|
+
# - Headings (`#` → `=`)
|
|
143
|
+
# - Bold (`**text**` → `*text*`)
|
|
144
|
+
# - Italic (`*text*` → `_text_`)
|
|
145
|
+
# - Links (`[text](url)` → `#link("url")[text]`)
|
|
146
|
+
# - Images (`` → `#image("url")`)
|
|
147
|
+
#
|
|
148
|
+
# @param markdown [String, nil] The Markdown text to convert
|
|
149
|
+
# @return [String] Typst-compatible markup, or empty string if markdown is nil
|
|
150
|
+
# @raise [ArgumentError] if markdown is not a String or nil
|
|
151
|
+
#
|
|
152
|
+
# @example Converting headings
|
|
153
|
+
# markdown_to_typst("# Title")
|
|
154
|
+
# # => "= Title"
|
|
155
|
+
#
|
|
156
|
+
# @example Converting subheadings
|
|
157
|
+
# markdown_to_typst("## Subtitle")
|
|
158
|
+
# # => "== Subtitle"
|
|
159
|
+
#
|
|
160
|
+
# @example Converting formatted text
|
|
161
|
+
# markdown_to_typst("**bold** and *italic*")
|
|
162
|
+
# # => "*bold* and _italic_"
|
|
163
|
+
#
|
|
164
|
+
# @example Converting links
|
|
165
|
+
# markdown_to_typst("[Typst](https://typst.app)")
|
|
166
|
+
# # => "#link(\"https://typst.app\")[Typst]"
|
|
167
|
+
#
|
|
168
|
+
# @see https://typst.app/docs/ Typst documentation
|
|
169
|
+
def markdown_to_typst(markdown)
|
|
170
|
+
return "" if markdown.nil?
|
|
171
|
+
raise ArgumentError, "markdown must be a String" unless markdown.is_a?(String)
|
|
172
|
+
|
|
173
|
+
result = markdown.dup
|
|
174
|
+
|
|
175
|
+
# Convert Markdown headers to Typst headers
|
|
176
|
+
# # Title -> = Title
|
|
177
|
+
# ## Subtitle -> == Subtitle
|
|
178
|
+
# etc.
|
|
179
|
+
result.gsub!(/^(#{Regexp.quote("#")}{1,6})\s+(.+)$/) do
|
|
180
|
+
"#{"=" * Regexp.last_match(1).length} #{Regexp.last_match(2)}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Convert Markdown bold to Typst bold, using placeholder bytes (\x01 text \x02)
|
|
184
|
+
# so the italic pass below doesn't re-convert the resulting *text* markers.
|
|
185
|
+
# **text** or __text__ -> *text*
|
|
186
|
+
result.gsub!(/(\*\*|__)(.+?)\1/, "\x01\\2\x02")
|
|
187
|
+
|
|
188
|
+
# Convert Markdown italic to Typst italic (underscore style)
|
|
189
|
+
# *text* or _text_ -> _text_
|
|
190
|
+
result.gsub!(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/, '_\1_')
|
|
191
|
+
result.gsub!(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/, '_\1_')
|
|
192
|
+
|
|
193
|
+
# Replace bold placeholders with Typst bold markers
|
|
194
|
+
result.gsub!("\x01", "*")
|
|
195
|
+
result.gsub!("\x02", "*")
|
|
196
|
+
|
|
197
|
+
# Convert Markdown code to Typst code
|
|
198
|
+
# `code` -> `code` (same in Typst)
|
|
199
|
+
|
|
200
|
+
# Convert Markdown images (must run before links, since 
|
|
201
|
+
# would otherwise be matched by the link pattern below)
|
|
202
|
+
#  -> #image("url")
|
|
203
|
+
result.gsub!(/!\[([^\]]*)\]\(([^)]+)\)/, '#image("\2")')
|
|
204
|
+
|
|
205
|
+
# Convert Markdown links
|
|
206
|
+
# [text](url) -> #link("url")[text]
|
|
207
|
+
result.gsub!(/\[([^\]]+)\]\(([^)]+)\)/, '#link("\2")[\1]')
|
|
208
|
+
|
|
209
|
+
result
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Reads and includes Markdown content, converting it to Typst syntax.
|
|
213
|
+
#
|
|
214
|
+
# This method is useful for including external Markdown files in Typst
|
|
215
|
+
# documents. The file is read and converted to Typst syntax.
|
|
216
|
+
#
|
|
217
|
+
# @param markdown_path [String] Path to the Markdown file (relative or absolute)
|
|
218
|
+
# @return [String] Typst-compatible content from the file
|
|
219
|
+
# @raise [ArgumentError] if markdown_path is not a String
|
|
220
|
+
# @raise [ArgumentError] if markdown_path is empty
|
|
221
|
+
# @raise [TypstRails::Error] if file not found
|
|
222
|
+
# @raise [TypstRails::Error] if permission denied
|
|
223
|
+
# @raise [TypstRails::Error] if file cannot be read
|
|
224
|
+
#
|
|
225
|
+
# @example Including a content file
|
|
226
|
+
# include_markdown("./content.md")
|
|
227
|
+
# # Returns the converted content of content.md
|
|
228
|
+
#
|
|
229
|
+
# @example Using in a Typst template
|
|
230
|
+
# # In your .typ.erb template:
|
|
231
|
+
# <%= include_markdown("sections/introduction.md") %>
|
|
232
|
+
#
|
|
233
|
+
# @note File path is relative to the current working directory
|
|
234
|
+
def include_markdown(markdown_path)
|
|
235
|
+
raise ArgumentError, "markdown_path must be a String" unless markdown_path.is_a?(String)
|
|
236
|
+
raise ArgumentError, "markdown_path cannot be empty" if markdown_path.empty?
|
|
237
|
+
|
|
238
|
+
begin
|
|
239
|
+
markdown_content = File.read(markdown_path)
|
|
240
|
+
markdown_to_typst(markdown_content)
|
|
241
|
+
rescue Errno::ENOENT
|
|
242
|
+
raise Error, "Markdown file not found: #{markdown_path}"
|
|
243
|
+
rescue Errno::EACCES
|
|
244
|
+
raise Error, "Permission denied reading Markdown file: #{markdown_path}"
|
|
245
|
+
rescue StandardError => e
|
|
246
|
+
raise Error, "Failed to read Markdown file #{markdown_path}: #{e.message}"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# MARK: - HTML Sanitization
|
|
251
|
+
|
|
252
|
+
DEFAULT_ALLOWED_TAGS = %w[
|
|
253
|
+
h1 h2 h3 h4 h5 h6 p br strong em u s del ins
|
|
254
|
+
ul ol li blockquote pre code a img table thead
|
|
255
|
+
tbody tr th td
|
|
256
|
+
].freeze
|
|
257
|
+
|
|
258
|
+
DEFAULT_ALLOWED_ATTRIBUTES = %w[href src alt title].freeze
|
|
259
|
+
|
|
260
|
+
# Sanitizes HTML before conversion to prevent XSS attacks.
|
|
261
|
+
#
|
|
262
|
+
# This method removes potentially dangerous tags and attributes from HTML
|
|
263
|
+
# before conversion to Typst. It provides basic XSS protection by:
|
|
264
|
+
# - Removing `<script>` and `<style>` tags
|
|
265
|
+
# - Removing event handler attributes (onclick, onload, etc.)
|
|
266
|
+
# - Optionally filtering to allowed tags and attributes
|
|
267
|
+
#
|
|
268
|
+
# @param html [String, nil] The HTML to sanitize
|
|
269
|
+
# @param allowed_tags [Array<String>, nil] List of allowed HTML tags (uses defaults if nil)
|
|
270
|
+
# @param allowed_attributes [Array<String>, nil] List of allowed attributes (uses defaults if nil)
|
|
271
|
+
# @return [String] Sanitized HTML, or empty string if html is nil
|
|
272
|
+
# @raise [ArgumentError] if html is not a String or nil
|
|
273
|
+
#
|
|
274
|
+
# @example Basic sanitization
|
|
275
|
+
# sanitize_html("<script>alert('xss')</script><p>Safe content</p>")
|
|
276
|
+
# # => "<p>Safe content</p>"
|
|
277
|
+
#
|
|
278
|
+
# @example Removing event handlers
|
|
279
|
+
# sanitize_html('<div onclick="evil()">Click</div>')
|
|
280
|
+
# # => "<div>Click</div>"
|
|
281
|
+
#
|
|
282
|
+
# @example Custom allowed tags
|
|
283
|
+
# sanitize_html("<p>Text</p><img src='x'>", allowed_tags: %w[p])
|
|
284
|
+
# # => "<p>Text</p>"
|
|
285
|
+
#
|
|
286
|
+
# @note This is basic sanitization. For production use with untrusted HTML,
|
|
287
|
+
# consider using a dedicated sanitization library like Loofah or Sanitize
|
|
288
|
+
def sanitize_html(html, allowed_tags: nil, allowed_attributes: nil)
|
|
289
|
+
return "" if html.nil?
|
|
290
|
+
raise ArgumentError, "html must be a String" unless html.is_a?(String)
|
|
291
|
+
|
|
292
|
+
allowed_tags ||= DEFAULT_ALLOWED_TAGS
|
|
293
|
+
allowed_attributes ||= DEFAULT_ALLOWED_ATTRIBUTES
|
|
294
|
+
|
|
295
|
+
fragment = Nokogiri::HTML5.fragment(html)
|
|
296
|
+
strip_disallowed_nodes(fragment, allowed_tags, allowed_attributes)
|
|
297
|
+
fragment.to_html
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
private
|
|
301
|
+
|
|
302
|
+
# Recursively removes tags not in +allowed_tags+ (keeping their text content)
|
|
303
|
+
# and strips attributes not in +allowed_attributes+ from the tags that remain.
|
|
304
|
+
def strip_disallowed_nodes(node, allowed_tags, allowed_attributes)
|
|
305
|
+
node.children.each do |child|
|
|
306
|
+
next strip_disallowed_nodes(child, allowed_tags, allowed_attributes) unless child.element?
|
|
307
|
+
|
|
308
|
+
unless allowed_tags.include?(child.name.downcase)
|
|
309
|
+
child.replace(child.children)
|
|
310
|
+
next strip_disallowed_nodes(node, allowed_tags, allowed_attributes)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
child.attribute_nodes.each do |attr|
|
|
314
|
+
attr.remove unless allowed_attributes.include?(attr.name.downcase)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
strip_disallowed_nodes(child, allowed_tags, allowed_attributes)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
public
|
|
322
|
+
|
|
323
|
+
# MARK: - URL Encoding
|
|
324
|
+
|
|
325
|
+
# URL-encodes text for safe use in links.
|
|
326
|
+
#
|
|
327
|
+
# Uses CGI.escape to encode special characters for URL safety.
|
|
328
|
+
# Useful when constructing URLs or query parameters in Typst templates.
|
|
329
|
+
#
|
|
330
|
+
# @param text [String, nil] The text to encode
|
|
331
|
+
# @return [String] URL-encoded text, or empty string if text is nil
|
|
332
|
+
# @raise [ArgumentError] if text is not a String or nil
|
|
333
|
+
#
|
|
334
|
+
# @example Encoding a search query
|
|
335
|
+
# url_encode("hello world")
|
|
336
|
+
# # => "hello+world"
|
|
337
|
+
#
|
|
338
|
+
# @example Encoding special characters
|
|
339
|
+
# url_encode("hello & goodbye")
|
|
340
|
+
# # => "hello+%26+goodbye"
|
|
341
|
+
#
|
|
342
|
+
# @example Building a URL
|
|
343
|
+
# query = url_encode(user_input)
|
|
344
|
+
# url = "https://example.com/search?q=#{query}"
|
|
345
|
+
#
|
|
346
|
+
# @see CGI.escape
|
|
347
|
+
def url_encode(text)
|
|
348
|
+
return "" if text.nil?
|
|
349
|
+
raise ArgumentError, "text must be a String" unless text.is_a?(String)
|
|
350
|
+
|
|
351
|
+
CGI.escape(text)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypstRails
|
|
4
|
+
# Integration for Rage framework
|
|
5
|
+
module RageIntegration
|
|
6
|
+
def self.setup
|
|
7
|
+
return unless defined?(::Rage)
|
|
8
|
+
|
|
9
|
+
require "typst_rails/renderer"
|
|
10
|
+
|
|
11
|
+
# Register Typst handler for Rage
|
|
12
|
+
::Rage.configure do |config|
|
|
13
|
+
# Rage uses a similar template system to Rails
|
|
14
|
+
# We can register our handler here when Rage's template system is available
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module TypstRails
|
|
6
|
+
class Railtie < ::Rails::Railtie
|
|
7
|
+
initializer "typst_rails.register_template_handler" do
|
|
8
|
+
ActiveSupport.on_load(:action_view) do
|
|
9
|
+
require "typst_rails/handler"
|
|
10
|
+
ActionView::Template.register_template_handler(:typ, TypstRails::Handler)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
rake_tasks do
|
|
15
|
+
load File.expand_path("../tasks/typst_rails/tasks.rake", __dir__)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|