Class: Asciidoctor::Ptc::Renderer

Inherits:
Object
  • Object
show all
Defined in:
lib/asciidoctor/ptc/renderer.rb

Overview

Renders .ptc (time-current grading) source into an image file by shelling out to the bundled Node helper (js/ptc_render.mjs). Handles output-directory resolution and content-hash caching so an unchanged study is only rendered once.

Unlike a renderer with no validation step of its own, a .ptc study can be well-formed AsciiDoc and still fail to parse or grade cleanly, so this class also carries the Node helper's verdict (resolved theme, whether the study had errors, whether grading failed, whether the sheet was force-rendered anyway) back to the caller — needed to label the generated image correctly, including on a cache hit, where the helper is not invoked at all.

Kept intentionally free of Asciidoctor-specific types so it is trivial to unit test in isolation.

Defined Under Namespace

Classes: Error

Constant Summary collapse

SCRIPT =
File.expand_path('js/ptc_render.mjs', __dir__)
FORMATS =
%w[svg png pdf].freeze
THEMES =
%w[light dark monochrome print].freeze
ORIENTATIONS =
%w[portrait landscape].freeze
SIZES =

Matches tc-curves' own PAPER_MM keys (src/export/export-pdf.ts).

%w[A0 A1 A2 A3 A4 A5 Letter Legal Tabloid].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Renderer

opts is a plain Hash with symbol keys:

:format, :scale, :width, :theme, :view, :background, :font_family,
:force, :all_views, :size, :orientation, :margin,
:node, :package_dir, :out_dir, :cache


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/asciidoctor/ptc/renderer.rb', line 40

def initialize(opts = {})
  @format      = normalize_format(opts[:format])
  @scale       = normalize_scale(opts[:scale])
  @width       = normalize_width(opts[:width])
  @theme       = normalize_theme(opts[:theme])
  @view        = presence(opts[:view])
  @background  = normalize_background(opts[:background])
  @font_family = presence(opts[:font_family])
  @force       = truthy(opts[:force], false)
  @all_views   = truthy(opts[:all_views], false)
  @size        = normalize_size(opts[:size])
  @orientation = normalize_orientation(opts[:orientation])
  @margin      = normalize_margin(opts[:margin])
  @node        = opts[:node] || 'node'
  @package_dir = opts[:package_dir]
  @out_dir     = opts[:out_dir] || Dir.pwd
  @cache       = opts.key?(:cache) ? opts[:cache] : true

  if @all_views && @format != 'pdf'
    raise Error, 'all-views needs format=pdf; an svg or png is a single image'
  end
end

Instance Attribute Details

#all_viewsObject (readonly)

Returns the value of attribute all_views.



64
65
66
# File 'lib/asciidoctor/ptc/renderer.rb', line 64

def all_views
  @all_views
end

#formatObject (readonly)

Returns the value of attribute format.



63
64
65
# File 'lib/asciidoctor/ptc/renderer.rb', line 63

def format
  @format
end

#grading_failsObject (readonly)

Returns the value of attribute grading_fails.



69
70
71
# File 'lib/asciidoctor/ptc/renderer.rb', line 69

def grading_fails
  @grading_fails
end

#has_errorsObject (readonly)

Returns the value of attribute has_errors.



69
70
71
# File 'lib/asciidoctor/ptc/renderer.rb', line 69

def has_errors
  @has_errors
end

#invalid_errorsObject (readonly)

Returns the value of attribute invalid_errors.



69
70
71
# File 'lib/asciidoctor/ptc/renderer.rb', line 69

def invalid_errors
  @invalid_errors
end

#marginObject (readonly)

Returns the value of attribute margin.



64
65
66
# File 'lib/asciidoctor/ptc/renderer.rb', line 64

def margin
  @margin
end

#orientationObject (readonly)

Returns the value of attribute orientation.



64
65
66
# File 'lib/asciidoctor/ptc/renderer.rb', line 64

def orientation
  @orientation
end

#out_dirObject (readonly)

Returns the value of attribute out_dir.



63
64
65
# File 'lib/asciidoctor/ptc/renderer.rb', line 63

def out_dir
  @out_dir
end

#resolved_heightObject (readonly)

Populated by #render (from the Node helper's summary, or from the cache sidecar on a cache hit).



68
69
70
# File 'lib/asciidoctor/ptc/renderer.rb', line 68

def resolved_height
  @resolved_height
end

#resolved_pagesObject (readonly)

Populated by #render (from the Node helper's summary, or from the cache sidecar on a cache hit).



68
69
70
# File 'lib/asciidoctor/ptc/renderer.rb', line 68

def resolved_pages
  @resolved_pages
end

#resolved_themeObject (readonly)

Populated by #render (from the Node helper's summary, or from the cache sidecar on a cache hit).



68
69
70
# File 'lib/asciidoctor/ptc/renderer.rb', line 68

def resolved_theme
  @resolved_theme
end

#resolved_widthObject (readonly)

Populated by #render (from the Node helper's summary, or from the cache sidecar on a cache hit).



68
69
70
# File 'lib/asciidoctor/ptc/renderer.rb', line 68

def resolved_width
  @resolved_width
end

#scaleObject (readonly)

Returns the value of attribute scale.



63
64
65
# File 'lib/asciidoctor/ptc/renderer.rb', line 63

def scale
  @scale
end

#sizeObject (readonly)

Returns the value of attribute size.



64
65
66
# File 'lib/asciidoctor/ptc/renderer.rb', line 64

def size
  @size
end

#themeObject (readonly)

Returns the value of attribute theme.



63
64
65
# File 'lib/asciidoctor/ptc/renderer.rb', line 63

def theme
  @theme
end

#viewObject (readonly)

Returns the value of attribute view.



63
64
65
# File 'lib/asciidoctor/ptc/renderer.rb', line 63

def view
  @view
end

#widthObject (readonly)

Returns the value of attribute width.



63
64
65
# File 'lib/asciidoctor/ptc/renderer.rb', line 63

def width
  @width
end

Instance Method Details

#command_for(out_path) ⇒ Object

Assemble the node command line (exposed for testing).



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/asciidoctor/ptc/renderer.rb', line 117

def command_for(out_path)
  cmd = [@node, SCRIPT,
         '--format', @format,
         '--scale', sprintf('%g', @scale),
         '--out', out_path]
  cmd.push('--width', @width.to_s) if @width
  cmd.push('--theme', @theme) if @theme
  cmd.push('--view', @view) if @view
  case @background
  when :transparent then cmd << '--transparent'
  when nil then nil
  else cmd.push('--background', @background)
  end
  cmd.push('--font-family', @font_family) if @font_family
  cmd << '--force' if @force
  cmd << '--all-views' if @all_views
  cmd.push('--size', @size) if @size
  cmd.push('--orientation', @orientation) if @orientation
  cmd.push('--margin', @margin.to_s) if @margin
  cmd.push('--package-dir', @package_dir) if @package_dir
  cmd
end

#render(source, basename = nil) ⇒ Object

Render source and return the basename of the generated file (which lives in out_dir). Pass basename (without extension) to get a stable, human-chosen file name instead of a content hash. Raises Renderer::Error on failure (a study with errors and no force, an unknown view, a rendering crash, ...).



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
# File 'lib/asciidoctor/ptc/renderer.rb', line 76

def render(source, basename = nil)
  FileUtils.mkdir_p(@out_dir)
  filename = target_filename(source, basename)
  path = File.join(@out_dir, filename)
  key = digest(source)

  if @cache && (meta = cached_meta(path, key))
    apply_meta(meta)
    return filename
  end

  cmd = command_for(path)
  stdout, stderr, status = Open3.capture3(*cmd, stdin_data: source, binmode: true)

  unless status.success?
    detail = stderr.to_s.strip
    detail = stdout.to_s.strip if detail.empty?
    raise Error, "PTC rendering failed (#{@format}): #{detail}"
  end
  unless File.file?(path)
    raise Error, "PTC renderer produced no output at #{path}: #{stderr.to_s.strip}"
  end

  warn_lines(stderr)
  meta = parse_summary(stdout)
  apply_meta(meta)
  write_cache_meta(path, key, meta) if @cache
  filename
end

#target_filename(source, basename = nil) ⇒ Object

The file name for source under the current options. Uses basename verbatim when given, otherwise a content-addressed name.



108
109
110
111
112
113
114
# File 'lib/asciidoctor/ptc/renderer.rb', line 108

def target_filename(source, basename = nil)
  if basename && !basename.to_s.strip.empty?
    "#{sanitize_basename(basename)}.#{@format}"
  else
    "ptc-#{digest(source)}.#{@format}"
  end
end