Module: Polytexnic::Utils

Extended by:
Utils
Included in:
Pipeline, Utils
Defined in:
lib/polytexnic/utils.rb

Instance Method Summary collapse

Instance Method Details

#add_font_info(string) ⇒ Object

Adds some verbatim font info (including size). We prepend rather than replace the styles because the Pygments output includes a required override of the default commandchars. Since the substitution is only important in the context of a PDF book, it only gets made if there’s a style in the ‘softcover.sty’ file. We also support custom overrides in ‘custom_pdf.sty’.



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/polytexnic/utils.rb', line 242

def add_font_info(string)
  softcover_sty  = File.join('latex_styles', 'softcover.sty')
  custom_pdf_sty = File.join('latex_styles', 'custom_pdf.sty')
  regex = '{code}{Verbatim}{(.*)}'
  styles = nil
  [softcover_sty, custom_pdf_sty].reverse.each do |filename|
    if File.exist?(filename)
      styles ||= File.read(filename).scan(/#{regex}/).flatten.first
    end
  end
  unless styles.nil?
    string.to_s.gsub!("\\begin{Verbatim}[",
                      "\\begin{Verbatim}[#{styles},")
  end
  string
end

#apple_silicon?Boolean

Returns true for Apple Silicon.

Returns:

  • (Boolean)


54
55
56
# File 'lib/polytexnic/utils.rb', line 54

def apple_silicon?
  RUBY_PLATFORM.match(/arm64/)
end

#cache_urls(doc, latex = false) ⇒ Object

Caches URLs for href and url commands.



106
107
108
109
110
111
112
113
114
115
# File 'lib/polytexnic/utils.rb', line 106

def cache_urls(doc, latex=false)
  doc.tap do |text|
    text.gsub!(/\\(href|url){(.*?)}/) do
      command, url = $1, $2
      key = digest(url)
      literal_cache[key] = url
      command == 'url' ? "\\href{#{key}}{#{url}}" : "\\href{#{key}}"
    end
  end
end

#debug?Boolean

Returns true if we are debugging, false otherwise. Manually change to ‘true` on an as-needed basis.

Returns:

  • (Boolean)


278
279
280
# File 'lib/polytexnic/utils.rb', line 278

def debug?
  false
end

#digest(string, options = {}) ⇒ Object

Returns a salted hash digest of the string.



79
80
81
82
# File 'lib/polytexnic/utils.rb', line 79

def digest(string, options = {})
  salt = options[:salt] || SecureRandom.base64
  Digest::SHA1.hexdigest("#{salt}--#{string}")
end

#escape_backslashes(string) ⇒ Object

Escapes backslashes. Interpolated backslashes need extra escaping. We only escape ‘\’ by itself, i.e., a backslash followed by spaces or the end of line.



101
102
103
# File 'lib/polytexnic/utils.rb', line 101

def escape_backslashes(string)
  string.gsub(/\\(\s+|$)/) { '\\\\' + $1.to_s }
end

#expand_input!(text, code_function, ext = 'md') ⇒ Object

Expands ‘input’ command by processing & inserting the target source. We skip tikzstyles since those don’t get inserted into the text.



39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/polytexnic/utils.rb', line 39

def expand_input!(text, code_function, ext = 'md')
  uuid = "52576c7eb84a49b5afc0d9913891f346"
  text.gsub!(/\\input\{(.*\.tikzstyles)\}/) { "#{uuid}-#{$1}-#{uuid}" }
  text.gsub!(/^[ \t]*\\input\{(.*?)\}[ \t]*$/) do
    # Prepend a newline for safety.
    included_text = "\n" + File.read("#{$1}.#{ext}")
    code_function.call(included_text).tap do |clean_text|
      # Recursively substitute '\input' in included text.
      expand_input!(clean_text, code_function, ext)
    end
  end
  text.gsub!(/#{uuid}-(.*)-#{uuid}/) { "\\input{#{$1}}" }
end

#framed(code) ⇒ Object

Puts a frame around code.



211
212
213
# File 'lib/polytexnic/utils.rb', line 211

def framed(code)
  "\\begin{framed_shaded}\n#{code}\n\\end{framed_shaded}"
end

#highlight(key, content, language, formatter, options) ⇒ Object

Highlights a code sample.



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/polytexnic/utils.rb', line 216

def highlight(key, content, language, formatter, options)
  require 'pygments'
  options = JSON.parse('{' + options.to_s + '}')
  if options['linenos'] && formatter == 'html'
    # Inline numbers look much better in HTML but are invalid in LaTeX.
    options['linenos'] = 'inline'
  end
  if (lines = options['hl_lines'])
    content_lines = content.split("\n")
    if lines.max > content_lines.length
      err  = "\nHighlight line(s) out of range: #{lines.inspect}\n"
      err += content
      raise err
    end
  end
  highlight_cache[key] ||= Pygments.highlight(content, lexer:     language,
                                                       formatter: formatter,
                                                       options:   options)
end

#highlight_lines(output, options) ⇒ Object

Highlight lines (i.e., with a yellow background). This is needed due to a Pygments bug that fails to highlight lines in the LaTeX output.



193
194
195
196
197
198
199
200
201
202
203
# File 'lib/polytexnic/utils.rb', line 193

def highlight_lines(output, options)
  highlighted_lines(options).each do |i|
    if i > output.length - 1
      $stderr.puts "Warning: Highlighted line #{i} out of range" unless test?
      $stderr.puts output.inspect unless test?
    else
      output[i] = '\setlength{\fboxsep}{0pt}\colorbox{hilightyellow}{' +
                  output[i] + '}'
    end
  end
end

#highlight_source_code(document) ⇒ Object

Highlights source code.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/polytexnic/utils.rb', line 165

def highlight_source_code(document)
  if document.is_a?(String) # LaTeX
    substitutions = {}
    document.tap do
      code_cache.each do |key, (content, language, in_codelisting, options)|
        code   = highlight(key, content, language, 'latex', options)
        output = code.split("\n")
        horrible_backslash_kludge(add_font_info(output.first))
        highlight_lines(output, options)
        code = output.join("\n")
        substitutions[key] = in_codelisting ? code : framed(code)
      end
      document.gsub!(Regexp.union(substitutions.keys), substitutions)
    end
  else # HTML
    document.css('div.code').each do |code_block|
      key = code_block.content
      next unless (value = code_cache[key])
      content, language, _, options = value
      code_block.inner_html = highlight(key, content, language, 'html',
                                        options)
    end
  end
end

#highlighted_lines(options) ⇒ Object

Returns an array with the highlighted lines.



206
207
208
# File 'lib/polytexnic/utils.rb', line 206

def highlighted_lines(options)
  JSON.parse('{' + options.to_s + '}')['hl_lines'] || []
end

#horrible_backslash_kludge(string) ⇒ Object

Does something horrible with backslashes. OK, so the deal is that code highlighted for LaTeX contains the line beginVerbatim Oh crap, there are backslashes in there. This means we have no chance of getting things to work after interpolating, gsubbing, and so on, because in Ruby ‘\foo’ is the same as ‘\\foo’, ‘}’ is ‘}’, etc. I thought I escaped (heh) this problem with the ‘escape_backslashes` method, but here the problem is extremely specific. In particular, \{} is really \ and { and }, but Ruby doesn’t know WTF to do with it, and thinks that it’s “\{}”, which is the same as ‘{}’. The solution is to replace ‘\\’ with some number of backslashes. How many? I literally had to just keep adding backslashes until the output was correct when running ‘softcover build:pdf`.



272
273
274
# File 'lib/polytexnic/utils.rb', line 272

def horrible_backslash_kludge(string)
  string.to_s.gsub!(/commandchars=\\\\/, 'commandchars=\\\\\\\\')
end

#linux?Boolean

Returns true if platform is Linux.

Returns:

  • (Boolean)


74
75
76
# File 'lib/polytexnic/utils.rb', line 74

def linux?
  RUBY_PLATFORM.match(/linux/)
end

#os_x?Boolean

Returns true if platform is OS X.

Returns:

  • (Boolean)


69
70
71
# File 'lib/polytexnic/utils.rb', line 69

def os_x?
  RUBY_PLATFORM.match(/darwin/)
end

#os_x_newer?Boolean

Returns true for OS X Mountain Lion (10.8) and later.

Returns:

  • (Boolean)


59
60
61
# File 'lib/polytexnic/utils.rb', line 59

def os_x_newer?
  os_x? && !os_x_older?
end

#os_x_older?Boolean

Returns true for OS X Lion (10.7) and earlier.

Returns:

  • (Boolean)


64
65
66
# File 'lib/polytexnic/utils.rb', line 64

def os_x_older?
  os_x? && RUBY_PLATFORM.include?('11')
end

#pipeline_digest(element) ⇒ Object

Returns a digest for passing things through the pipeline.



85
86
87
88
# File 'lib/polytexnic/utils.rb', line 85

def pipeline_digest(element)
  value = digest("#{Time.now.to_s}::#{element}")
  @literal_cache[element.to_s] ||= value
end

#profiling?Boolean

Returns true if we are profiling the code, false otherwise. Manually change to ‘true` on an as-needed basis.

Returns:

  • (Boolean)


284
285
286
287
# File 'lib/polytexnic/utils.rb', line 284

def profiling?
  return false if test?
  false
end

#set_test_mode!Object



289
290
291
# File 'lib/polytexnic/utils.rb', line 289

def set_test_mode!
  @@test_mode = true
end

#test?Boolean

Returns:

  • (Boolean)


293
294
295
# File 'lib/polytexnic/utils.rb', line 293

def test?
  defined?(@@test_mode) && @@test_mode
end

#tralicsObject

Returns the executable for the Tralics LaTeX-to-XML converter.



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/polytexnic/utils.rb', line 10

def tralics
  executable = `which tralics`.chomp
  return executable unless executable.empty?
  filename = if apple_silicon?
               'tralics-apple-silicon'
             elsif os_x_newer?
               'tralics-os-x-newer'
             elsif os_x_older?
               'tralics-os-x-older'
             elsif linux?
               "tralics-#{RUBY_PLATFORM}"
             end
  project_root = File.join(File.dirname(__FILE__), '..', '..')
  executable = File.join(project_root, 'precompiled_binaries', filename)
  output = `#{executable}`
  unless output.include?('This is tralics')
    url = 'https://github.com/softcover/tralics'
    $stderr.puts "\nError: Document not built"
    $stderr.puts "No compatible Tralics LaTeX-to-XML translator found"
    $stderr.puts "Follow the instructions at\n  #{url}\n"
    $stderr.puts "to compile tralics and put it on your path"
    exit(1)
  end
  @tralics ||= executable
end

#tralics_commandsObject

Returns some commands for Tralics. For various reasons, we don’t actually want to include these in the style file that gets passed to LaTeX. For example, the commands with ‘xmlelt’ aren’t even valid LaTeX; they’re actually pseudo-LaTeX that has special meaning to the Tralics processor.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/polytexnic/utils.rb', line 132

def tralics_commands
  base_commands = <<-'EOS'
% Commands specific to Tralics
\def\hyperref[#1]#2{\xmlelt{a}{\XMLaddatt{target}{#1}#2}}
\newcommand{\heading}[1]{\xmlelt{heading}{#1}}
\newcommand{\codecaption}[1]{\xmlelt{heading}{#1}}
\newcommand{\sout}[1]{\xmlelt{sout}{#1}}
\newcommand{\kode}[1]{\xmlelt{kode}{#1}}
\newcommand{\coloredtext}[2]{\xmlelt{coloredtext}{\AddAttToCurrent{color}{#1}#2}}
\newcommand{\coloredtexthtml}[2]{\xmlelt{coloredtexthtml}{\AddAttToCurrent{color}{#1}#2}}
\newcommand{\filepath}[1]{\xmlelt{filepath}{#1}}
\newcommand{\image}[1]{\xmlelt{image}{#1}}
\newcommand{\imagebox}[1]{\xmlelt{imagebox}{#1}}
% Ignore pbox argument, just replacing with content.
\newcommand{\pbox}[2]{#2}
% Ignore some other commands.
\newcommand{\includepdf}[1]{}
\newcommand{\newunicodechar}[2]{}
\newcommand{\extrafloats}[1]{}
  EOS
  custom = <<-EOS
\\usepackage{amsthm}
\\newtheorem{codelisting}{#{language_labels["listing"]}}[chapter]
\\newtheorem{aside}{#{language_labels["aside"]}}[chapter]
\\newtheorem{theorem}{#{language_labels["theorem"]}}[chapter]
  EOS
  (@supported_theorem_types - ["theorem"]).each do |lab|
    custom += "\\newtheorem{#{lab}}[theorem]{#{language_labels[lab]}}\n"
  end
  [base_commands, custom].join("\n")
end

#underscore_digestObject

Returns a digest for use in labels. I like to use labels of the form cha:foo_bar, but for some reason Tralics removes the underscore in this case.



93
94
95
# File 'lib/polytexnic/utils.rb', line 93

def underscore_digest
  pipeline_digest('_')
end

#xmlelement(name, skip = false) ⇒ Object

Returns a Tralics pseudo-LaTeX XML element. The use of the ‘skip’ flag is a hack to be able to use xmlelement even when generating, e.g., LaTeX, where we simply want to yield the block.



121
122
123
124
125
# File 'lib/polytexnic/utils.rb', line 121

def xmlelement(name, skip = false)
  output = (skip ? "" : "\\begin{xmlelement}{#{name}}")
  output << yield if block_given?
  output << (skip ? "" : "\\end{xmlelement}")
end