antex 0.1.1
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/LICENSE.txt +21 -0
- data/README.md +10 -0
- data/lib/antex.rb +13 -0
- data/lib/antex/command.rb +93 -0
- data/lib/antex/defaults.yml +73 -0
- data/lib/antex/error.rb +5 -0
- data/lib/antex/job.rb +117 -0
- data/lib/antex/liquid_helpers.rb +49 -0
- data/lib/antex/measurable.rb +65 -0
- data/lib/antex/set_box.rb +81 -0
- data/lib/antex/svg_box.rb +22 -0
- data/lib/antex/tex_box.rb +30 -0
- data/lib/antex/version.rb +5 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9e8f4f3fd672813d3ad879df973fd7f322a74690
|
4
|
+
data.tar.gz: 84e7aa764ab967c2afc9423ce92b03a04e4d5e2e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 586cb677c67b145dd111eaf9056d3bca2b4739c64be9cf7bf42336bcb75ed71f6bab884fd47932bbc79c10a70fa4a823c74f74b0622f12be73041ccd724439bc
|
7
|
+
data.tar.gz: 42335c58867725e0d1959e96b93a2b1a21a867f6943daeea39a39527ab4145c912fd1728d3e4038955224e191d3e7f20948222ec0158c381b94ed4a73c414d9c
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Paolo Brasolin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# antex
|
2
|
+
|
3
|
+
[](https://travis-ci.org/paolobrasolin/antex)
|
4
|
+
[](https://github.com/paolobrasolin/antex)
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
6
|
+
|
7
|
+
[](https://codeclimate.com/github/paolobrasolin/antex)
|
8
|
+
[](https://codeclimate.com/github/paolobrasolin/antex/coverage)
|
9
|
+
[](http://inch-ci.org/github/paolobrasolin/antex)
|
10
|
+
[](https://codeclimate.com/github/paolobrasolin/antex)
|
data/lib/antex.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'antex/version'
|
4
|
+
require 'antex/error'
|
5
|
+
|
6
|
+
require 'antex/measurable'
|
7
|
+
require 'antex/tex_box'
|
8
|
+
require 'antex/svg_box'
|
9
|
+
require 'antex/set_box'
|
10
|
+
|
11
|
+
require 'antex/liquid_helpers'
|
12
|
+
require 'antex/job'
|
13
|
+
require 'antex/command'
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
module Antex
|
6
|
+
# This class encapsulates the execution of a command line that needs
|
7
|
+
# some source files to produce some target files.
|
8
|
+
#
|
9
|
+
# Existence checks are performed on files:
|
10
|
+
# * if all targets exist before execution then it is skipped
|
11
|
+
# * all sources must exist before execution
|
12
|
+
# * all targets must exist after execution
|
13
|
+
#
|
14
|
+
# Details of the execution itself are given by
|
15
|
+
# the attributes {#stdout}, {#stderr} and {#status}.
|
16
|
+
class Command
|
17
|
+
class MissingSourceFiles < Error; end
|
18
|
+
class MissingTargetFiles < Error; end
|
19
|
+
class ExecutionFailed < Error; end
|
20
|
+
|
21
|
+
# @return [String, nil] the +stdout+ returned by the command line
|
22
|
+
attr_reader :stdout
|
23
|
+
|
24
|
+
# @return [String, nil] the +stderr+ returned by the command line
|
25
|
+
attr_reader :stderr
|
26
|
+
|
27
|
+
# @return [Process::Status, nil] the +status+ returned by the command line
|
28
|
+
attr_reader :status
|
29
|
+
|
30
|
+
# Initializes the command.
|
31
|
+
#
|
32
|
+
# @note A command with no targets will always skip
|
33
|
+
# since it needs to produce nothing!
|
34
|
+
#
|
35
|
+
# @param name [String] name of the command (just for error reporting)
|
36
|
+
# @param sources [Array] list of source files
|
37
|
+
# @param targets [Array] list of target files
|
38
|
+
# @param command_line [String] command line that will be executed
|
39
|
+
def initialize(name:, sources:, targets:, command_line:)
|
40
|
+
@name = name
|
41
|
+
@sources = sources
|
42
|
+
@targets = targets
|
43
|
+
@command_line = command_line
|
44
|
+
end
|
45
|
+
|
46
|
+
# Executes the command.
|
47
|
+
#
|
48
|
+
# @raise [MissingSourceFiles] when source files are missing
|
49
|
+
# @raise [ExecutionFailed] when command execution fails
|
50
|
+
# @raise [MissingTargetFiles] when command does not create target files
|
51
|
+
def run!
|
52
|
+
return if all_exist? @targets
|
53
|
+
check_source_files!
|
54
|
+
@stdout, @stderr, @status = Open3.capture3 @command_line
|
55
|
+
check_status!
|
56
|
+
check_target_files!
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def all_exist?(files)
|
62
|
+
files.all?(&File.method(:exist?))
|
63
|
+
end
|
64
|
+
|
65
|
+
def missing(files)
|
66
|
+
files.reject(&File.method(:exist?))
|
67
|
+
end
|
68
|
+
|
69
|
+
def check_source_files!
|
70
|
+
raise MissingSourceFiles, <<~MISSING_SOURCE unless all_exist? @sources
|
71
|
+
Required source files #{missing @sources} for command #{@name} are missing.
|
72
|
+
MISSING_SOURCE
|
73
|
+
end
|
74
|
+
|
75
|
+
def check_status!
|
76
|
+
raise ExecutionFailed, <<~EXECUTION_FAILED unless @status.success?
|
77
|
+
Command #{@name} failed.
|
78
|
+
Command line: #{@command_line}
|
79
|
+
Status: #{@status}
|
80
|
+
Stdout:
|
81
|
+
#{@stdout}
|
82
|
+
Stderr:
|
83
|
+
#{@stderr}
|
84
|
+
EXECUTION_FAILED
|
85
|
+
end
|
86
|
+
|
87
|
+
def check_target_files!
|
88
|
+
raise MissingTargetFiles, <<~MISSING_TARGET unless all_exist? @targets
|
89
|
+
Expected target files #{missing @targets} were not produced by command #{@name}.
|
90
|
+
MISSING_TARGET
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
template: |
|
2
|
+
\documentclass{article}
|
3
|
+
\pagestyle{empty}
|
4
|
+
{{ preamble }}
|
5
|
+
\newsavebox\snippet
|
6
|
+
\begin{lrbox}{\snippet}{{ prepend }}{{ snippet }}{{ append }}\end{lrbox}
|
7
|
+
\newwrite\file
|
8
|
+
\immediate\openout\file=\jobname.yml
|
9
|
+
\immediate\write\file{em: \the\dimexpr1em}
|
10
|
+
\immediate\write\file{ex: \the\dimexpr1ex}
|
11
|
+
\immediate\write\file{ht: \the\ht\snippet}
|
12
|
+
\immediate\write\file{dp: \the\dp\snippet}
|
13
|
+
\immediate\write\file{wd: \the\wd\snippet}
|
14
|
+
\closeout\file
|
15
|
+
\begin{document}\usebox{\snippet}\end{document}
|
16
|
+
preamble:
|
17
|
+
prepend:
|
18
|
+
append:
|
19
|
+
dirs:
|
20
|
+
work: .antex-cache
|
21
|
+
files:
|
22
|
+
tex: "{{ dir.work }}/{{ hash }}.tex"
|
23
|
+
dvi: "{{ dir.work }}/{{ hash }}.dvi"
|
24
|
+
yml: "{{ dir.work }}/{{ hash }}.yml"
|
25
|
+
tfm: "{{ dir.work }}/{{ hash }}.tfm.svg"
|
26
|
+
fit: "{{ dir.work }}/{{ hash }}.fit.svg"
|
27
|
+
svg: "{{ dir.work }}/{{ hash }}.svg"
|
28
|
+
pipeline:
|
29
|
+
- latexmk
|
30
|
+
- dvisvgm_tfm
|
31
|
+
- dvisvgm_fit
|
32
|
+
- rename
|
33
|
+
commands:
|
34
|
+
latexmk:
|
35
|
+
command:
|
36
|
+
- "latexmk"
|
37
|
+
- "-output-directory={{ dir.work }}"
|
38
|
+
- "{{ file.tex }}"
|
39
|
+
sources:
|
40
|
+
- "{{ file.tex }}"
|
41
|
+
targets:
|
42
|
+
- "{{ file.dvi }}"
|
43
|
+
- "{{ file.yml }}"
|
44
|
+
dvisvgm_tfm:
|
45
|
+
command:
|
46
|
+
- "dvisvgm"
|
47
|
+
- "--no-fonts"
|
48
|
+
- "{{ file.dvi }}"
|
49
|
+
- "--output={{ file.tfm }}"
|
50
|
+
sources:
|
51
|
+
- "{{ file.dvi }}"
|
52
|
+
targets:
|
53
|
+
- "{{ file.tfm }}"
|
54
|
+
dvisvgm_fit:
|
55
|
+
command:
|
56
|
+
- "dvisvgm"
|
57
|
+
- "--no-fonts"
|
58
|
+
- "--exact"
|
59
|
+
- "{{ file.dvi }}"
|
60
|
+
- "--output={{ file.fit }}"
|
61
|
+
sources:
|
62
|
+
- "{{ file.dvi }}"
|
63
|
+
targets:
|
64
|
+
- "{{ file.fit }}"
|
65
|
+
rename:
|
66
|
+
command:
|
67
|
+
- "cp"
|
68
|
+
- "{{ file.fit }}"
|
69
|
+
- "{{ file.svg }}"
|
70
|
+
sources:
|
71
|
+
- "{{ file.fit }}"
|
72
|
+
targets:
|
73
|
+
- "{{ file.svg }}"
|
data/lib/antex/error.rb
ADDED
data/lib/antex/job.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Antex
|
4
|
+
# This class is the beating heart of {Antex}.
|
5
|
+
#
|
6
|
+
# The simplest usage of this gem involves three steps:
|
7
|
+
# # 1. Get a LaTeX snippet and initialize a job.
|
8
|
+
# job = Antex::Job.new snippet: "Hello, \TeX world!"
|
9
|
+
# # 2. Run the job!
|
10
|
+
# job.run!
|
11
|
+
# # 3. Do something neat with your new SVG file and its typesetting metrics.
|
12
|
+
# job.files(:svg) # => "./.antex-cache/eb64793dafe3bbd963dc663385a22096.svg"
|
13
|
+
# job.set_box.measures # => {:ex=>1, :wd=>17.03..., :mt=>-0.05..., ...}
|
14
|
+
#
|
15
|
+
# The {Antex::Job::DEFAULTS} are tuned to work with a basic
|
16
|
+
# +latexmk+/+dvisvgm+ pipeline.
|
17
|
+
# To get started with customization. you'll want to read the (not yet well
|
18
|
+
# documented) +YAML+ source loaded into the constant at the
|
19
|
+
# {https://github.com/paolobrasolin/antex/blob/master/lib/antex/defaults.yml
|
20
|
+
# github repo}.
|
21
|
+
class Job
|
22
|
+
include LiquidHelpers
|
23
|
+
|
24
|
+
# The configuration defaults for a +latexmk+/+dvisvgm+ pipeline.
|
25
|
+
DEFAULTS =
|
26
|
+
YAML.load_file(File.join(File.dirname(__FILE__), 'defaults.yml')).freeze
|
27
|
+
|
28
|
+
# @return [Hash] the initialization options
|
29
|
+
attr_reader :options
|
30
|
+
|
31
|
+
# @return [Hash] the dirs paths rendered from the options
|
32
|
+
attr_reader :dirs
|
33
|
+
|
34
|
+
# @return [Hash] the files paths rendered from the options
|
35
|
+
attr_reader :files
|
36
|
+
|
37
|
+
# @return [String] the unique hash identifying the job
|
38
|
+
attr_reader :hash
|
39
|
+
|
40
|
+
# @return [SetBox, nil] the {SetBox} calculated by the {#run!}
|
41
|
+
attr_reader :set_box
|
42
|
+
|
43
|
+
# @param snippet [String] +TeX+ code snippet to render
|
44
|
+
# @param options [Hash] job options
|
45
|
+
def initialize(snippet: '', options: Antex::Job::DEFAULTS)
|
46
|
+
@options = options
|
47
|
+
@snippet = snippet
|
48
|
+
|
49
|
+
prepare_code
|
50
|
+
prepare_hash
|
51
|
+
prepare_dirs
|
52
|
+
prepare_files
|
53
|
+
end
|
54
|
+
|
55
|
+
# Run the job, compile the +TeX+ snippet and calculate the {#set_box}.
|
56
|
+
def run!
|
57
|
+
create_dirs
|
58
|
+
write_code
|
59
|
+
run_pipeline!
|
60
|
+
load_set_box
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def prepare_code
|
66
|
+
@code = liquid_render @options['template'],
|
67
|
+
'preamble' => @options['preamble'],
|
68
|
+
'append' => @options['append'],
|
69
|
+
'prepend' => @options['prepend'],
|
70
|
+
'snippet' => @snippet
|
71
|
+
end
|
72
|
+
|
73
|
+
def prepare_hash
|
74
|
+
@hash = Digest::MD5.hexdigest @code
|
75
|
+
end
|
76
|
+
|
77
|
+
def prepare_dirs
|
78
|
+
@dirs = liquid_render @options['dirs'],
|
79
|
+
'hash' => @hash
|
80
|
+
end
|
81
|
+
|
82
|
+
def prepare_files
|
83
|
+
@files = liquid_render @options['files'],
|
84
|
+
'hash' => @hash,
|
85
|
+
'dir' => @dirs
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_dirs
|
89
|
+
@dirs.each_value { |path| FileUtils.mkdir_p path }
|
90
|
+
end
|
91
|
+
|
92
|
+
def write_code
|
93
|
+
return if File.exist? @files['tex']
|
94
|
+
File.open(@files['tex'], 'w') { |io| io.write @code }
|
95
|
+
end
|
96
|
+
|
97
|
+
def run_pipeline!
|
98
|
+
context = { 'hash' => @hash, 'dir' => @dirs, 'file' => @files }
|
99
|
+
pipeline = liquid_render @options['pipeline'], context
|
100
|
+
commands = liquid_render @options['commands'], context
|
101
|
+
|
102
|
+
pipeline.each do |command_name|
|
103
|
+
options = { name: command_name,
|
104
|
+
sources: commands[command_name]['sources'],
|
105
|
+
targets: commands[command_name]['targets'],
|
106
|
+
command_line: commands[command_name]['command'].join(' ') }
|
107
|
+
Command.new(options).run!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def load_set_box
|
112
|
+
@set_box = SetBox.new.load yml: @files['yml'],
|
113
|
+
tfm: @files['tfm'],
|
114
|
+
fit: @files['fit']
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'liquid'
|
4
|
+
|
5
|
+
module Antex
|
6
|
+
# Exposes helper methods to simplify +Liquid+ templates rendering.
|
7
|
+
module LiquidHelpers
|
8
|
+
class UnknownClass < Error; end
|
9
|
+
|
10
|
+
# Recursively renders +Liquid+ template strings, possibly organized in
|
11
|
+
# nested arrays and hashes, using the given hash of contextual variables.
|
12
|
+
#
|
13
|
+
# @param object [String, Array, Hash] the object to render
|
14
|
+
# @param context_hash [Hash]
|
15
|
+
# the context hash accessible from the object strings
|
16
|
+
# @return [String] the rendered object
|
17
|
+
# @raise [UnknownClass] when given anything that's not renderable
|
18
|
+
def liquid_render(object, context_hash = {})
|
19
|
+
case object
|
20
|
+
when String
|
21
|
+
liquid_render_string object, context_hash
|
22
|
+
when Array
|
23
|
+
liquid_render_array object, context_hash
|
24
|
+
when Hash
|
25
|
+
liquid_render_hash object, context_hash
|
26
|
+
else
|
27
|
+
raise UnknownClass, "I don't know how to render a #{object.class}."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def liquid_render_string(string, context_hash = {})
|
34
|
+
Liquid::Template.parse(string).render(context_hash)
|
35
|
+
end
|
36
|
+
|
37
|
+
def liquid_render_array(array, context_hash = {})
|
38
|
+
array.map do |element|
|
39
|
+
liquid_render element, context_hash
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def liquid_render_hash(hash, context_hash = {})
|
44
|
+
hash.map do |key, value|
|
45
|
+
[key, liquid_render(value, context_hash)]
|
46
|
+
end.to_h
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Antex
|
4
|
+
# Implements an easy to use container for storing measures
|
5
|
+
# and retrieving them performing unit conversions.
|
6
|
+
#
|
7
|
+
# Measures should be a hash with symbolic keys
|
8
|
+
# and numeric values with uniform units.
|
9
|
+
# mea = Measurable.new
|
10
|
+
# mea.measures = { km: 1, mi: 0.621 }
|
11
|
+
class Measurable
|
12
|
+
class InvalidUnit < Antex::Error; end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@default_unit = nil
|
16
|
+
@measures = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Symbol] the default unit
|
20
|
+
attr_reader :default_unit
|
21
|
+
|
22
|
+
def default_unit=(unit)
|
23
|
+
raise InvalidUnit, "Unit #{unit} is undefined." unless @measures.key? unit
|
24
|
+
raise InvalidUnit, "Unit #{unit} is zero." if @measures[unit].zero?
|
25
|
+
@default_unit = unit
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Hash] the stored measures
|
29
|
+
# @note When setting, the default unit will be set to +nil+.
|
30
|
+
attr_reader :measures
|
31
|
+
|
32
|
+
def measures=(**measures)
|
33
|
+
@default_unit = nil
|
34
|
+
@measures = measures
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def calculate(metric, unit = @default_unit)
|
40
|
+
raise InvalidUnit, 'Default unit is not set.' if unit.nil?
|
41
|
+
raise InvalidUnit, "Unit #{unit} is undefined." unless @measures.key? unit
|
42
|
+
raise InvalidUnit, "Unit #{unit} is zero." if @measures[unit].zero?
|
43
|
+
@measures[metric].fdiv @measures[unit]
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_to_missing?(method_name, include_private = false)
|
47
|
+
(@measures.include? method_name) || super
|
48
|
+
end
|
49
|
+
|
50
|
+
# Once measures are set they can be accessed and converted.
|
51
|
+
# mea.km(:km) # => 1.0
|
52
|
+
# mea.km(:mi) # => 1.610...
|
53
|
+
# mea.mi(:mi) # => 1.0
|
54
|
+
# mea.mi(:km) # => 0.621
|
55
|
+
#
|
56
|
+
# You can set a default unit for quicker access.
|
57
|
+
# mea.default_unit = :mi
|
58
|
+
# mea.km # => 1.610...
|
59
|
+
# mea.mi # => 1.0
|
60
|
+
def method_missing(method_name, *arguments, &block)
|
61
|
+
return super unless @measures.include? method_name
|
62
|
+
send :calculate, method_name, *arguments, &block
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Antex
|
4
|
+
# Encapsulates calculations and results for perfectly typesetting
|
5
|
+
# the +SVG+ vectorial conversion of a +TeX+ box.
|
6
|
+
#
|
7
|
+
# Three files are required by {#load} for initialization:
|
8
|
+
# [+YAML+ file containing +TeX+ measures]
|
9
|
+
# We obtain this by using +TeX+ directly
|
10
|
+
# to measure the box and write to file.
|
11
|
+
# Required metrics are +ht+, +dp+ and +wd+ (naturally expressed in +pt+s).
|
12
|
+
# [+SVG+ picture fitting +tfm+ metrics]
|
13
|
+
# We obtain this by converting the +DVI+ rendition of the box
|
14
|
+
# to +SVG+ using {http://dvisvgm.bplaced.net/ +dvisvgm+}.
|
15
|
+
# [+SVG+ picture fitting the ink]
|
16
|
+
# We obtain this by converting the +DVI+ rendition of the box
|
17
|
+
# to +SVG+ using {http://dvisvgm.bplaced.net/ +dvisvgm+}
|
18
|
+
# with the +--exact+ flag.
|
19
|
+
#
|
20
|
+
# After the initialization the calculations are performed immediatly.
|
21
|
+
# Six metrics become available:
|
22
|
+
# * +mt+, +mr+, +ml+ and +ml+ (the margins, positive or negative)
|
23
|
+
# * +wd+ and +ht+ (width and height)
|
24
|
+
# * +ex+ (the default unit)
|
25
|
+
class SetBox < Measurable
|
26
|
+
class InvalidMeasure < Antex::Error; end
|
27
|
+
|
28
|
+
# @param yml [String] path of the +YAML+ file containing +TeX+ measures
|
29
|
+
# @param tfm [String] path of the +SVG+ picture fitting +tfm+ metrics
|
30
|
+
# @param fit [String] path of the +SVG+ picture fitting the ink
|
31
|
+
# @return [TexBox] returns +self+ after loading
|
32
|
+
def load(yml:, tfm:, fit:)
|
33
|
+
@tex = TexBox.new.load yml
|
34
|
+
@tfm = SVGBox.new.load tfm
|
35
|
+
@fit = SVGBox.new.load fit
|
36
|
+
@tex.default_unit = :ex
|
37
|
+
check_measures!
|
38
|
+
self.measures = compute_measures
|
39
|
+
self.default_unit = :ex
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Memorandum:
|
46
|
+
# .--> x ml fit.dx mr
|
47
|
+
# | |--|-------------|--|
|
48
|
+
# v y
|
49
|
+
# - ,-------------------. -
|
50
|
+
# mt | | TFM | |
|
51
|
+
# - | ,-------------. | | tfm.dy
|
52
|
+
# | | | FIT | | |
|
53
|
+
# | | | | | |
|
54
|
+
# fit.dy | | | | | | r ,-------. - tex.ht
|
55
|
+
# | | | | | | <---> | TEX | |
|
56
|
+
# | |- |- - - - - - -| -| | |- - - -| -
|
57
|
+
# - | `-------------' | | `-------' - tex.dp
|
58
|
+
# mb | | | | |-------|
|
59
|
+
# - `-------------------' - tex.wd
|
60
|
+
# |-------------------|
|
61
|
+
# tfm.dx
|
62
|
+
def compute_measures # rubocop:disable Metrics/AbcSize
|
63
|
+
ex_px = (@tex.ht + @tex.dp) / @tfm.dy # [ex/px]
|
64
|
+
{
|
65
|
+
ex: 1,
|
66
|
+
th: ex_px * @fit.dy,
|
67
|
+
wd: ex_px * @fit.dx,
|
68
|
+
ml: ex_px * (- @tfm.ox + @fit.ox),
|
69
|
+
mt: ex_px * (- @tfm.oy + @fit.oy),
|
70
|
+
mr: ex_px * (+ @tfm.ox - @fit.ox + @tfm.dx - @fit.dx),
|
71
|
+
mb: ex_px * (+ @tfm.oy - @fit.oy + @tfm.dy - @fit.dy) - @tex.dp
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def check_measures!
|
76
|
+
raise InvalidMeasure, <<~INVALID_MEASURE if @tfm.dy.zero?
|
77
|
+
The given tfm SVG file has zero height.
|
78
|
+
INVALID_MEASURE
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module Antex
|
6
|
+
# Loads and manages measures for +SVG+ pictures.
|
7
|
+
class SVGBox < Measurable
|
8
|
+
# Loads an +SVG+ file and extracts the measures of its +viewBox+.
|
9
|
+
#
|
10
|
+
# @param filepath [String] the path of the SVG file to load
|
11
|
+
# @return [SVGBox] returns +self+ after loading
|
12
|
+
def load(filepath)
|
13
|
+
svg_ast = Nokogiri::XML.parse File.read(filepath)
|
14
|
+
view_box = svg_ast.css('svg').attribute('viewBox')
|
15
|
+
magnitudes = view_box.to_s.split(' ').map(&:to_f)
|
16
|
+
@measures = %i[ox oy dx dy].zip(magnitudes).to_h
|
17
|
+
@measures[:px] ||= 1.0
|
18
|
+
@default_unit = :px
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Antex
|
6
|
+
# Loads and manages measures for +TeX+ boxes.
|
7
|
+
class TexBox < Measurable
|
8
|
+
# Loads a +YAML+ file containing +TeX+ measures.
|
9
|
+
#
|
10
|
+
# The expected input is a hash of named +pt+ lenghts.
|
11
|
+
# E.g.:
|
12
|
+
#
|
13
|
+
# pt: 1.0pt
|
14
|
+
# wd: 5.0pt
|
15
|
+
# ht: 8.0pt
|
16
|
+
# dp: 2.0pt
|
17
|
+
#
|
18
|
+
# @param filepath [String] the path of the YAML file to load
|
19
|
+
# @return [TexBox] returns +self+ after loading
|
20
|
+
def load(filepath)
|
21
|
+
yaml_hash = YAML.safe_load File.read(filepath)
|
22
|
+
units = yaml_hash.keys.map(&:to_sym)
|
23
|
+
magnitudes = yaml_hash.values.map { |value| value.chomp('pt').to_f }
|
24
|
+
@measures = units.zip(magnitudes).to_h
|
25
|
+
@measures[:pt] ||= 1.0
|
26
|
+
@default_unit = :pt
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: antex
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Paolo Brasolin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-09-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: liquid
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: guard
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.14'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.14'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: guard-rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.7'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.7'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.15'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.15'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.6'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.6'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.50'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.50'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: yard
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.9'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.9'
|
125
|
+
description: 'anTeX implements a universal TeX integration pipeline to easily embed
|
126
|
+
and render arbitrary code using any engine and dialect. '
|
127
|
+
email: paolo.brasolin@gmail.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- LICENSE.txt
|
133
|
+
- README.md
|
134
|
+
- lib/antex.rb
|
135
|
+
- lib/antex/command.rb
|
136
|
+
- lib/antex/defaults.yml
|
137
|
+
- lib/antex/error.rb
|
138
|
+
- lib/antex/job.rb
|
139
|
+
- lib/antex/liquid_helpers.rb
|
140
|
+
- lib/antex/measurable.rb
|
141
|
+
- lib/antex/set_box.rb
|
142
|
+
- lib/antex/svg_box.rb
|
143
|
+
- lib/antex/tex_box.rb
|
144
|
+
- lib/antex/version.rb
|
145
|
+
homepage: https://github.com/paolobrasolin/antex
|
146
|
+
licenses:
|
147
|
+
- MIT
|
148
|
+
metadata: {}
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
require_paths:
|
152
|
+
- lib
|
153
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '2.3'
|
158
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
requirements: []
|
164
|
+
rubyforge_project:
|
165
|
+
rubygems_version: 2.6.13
|
166
|
+
signing_key:
|
167
|
+
specification_version: 4
|
168
|
+
summary: Universal TeX integrator
|
169
|
+
test_files: []
|