# frozen_string_literal: true
require_relative 'spec_helper'
describe 'Asciidoctor::PDF::Converter - Source' do
context 'Rouge' do
it 'should use plain text lexer if language is not recognized' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,foobar]
----
puts "Hello, World!"
----
EOS
puts_text = (pdf.find_text 'puts')[0]
(expect puts_text).to be_nil
(expect pdf.text).to have_size 1
(expect pdf.text[0][:font_color]).to eql '333333'
end
it 'should expand tabs to preserve indentation' do
pdf = to_pdf <<~EOS, analyze: true
:source-highlighter: rouge
[source,c]
----
int main() {
\tevent_loop();
\treturn 0;
}
----
EOS
lines = pdf.lines
(expect lines).to have_size 4
(expect lines[1]).to eql %(\u00a0 event_loop();)
(expect lines[2]).to eql %(\u00a0 return 0;)
end
it 'should expand tabs used for column alignment' do
pdf = to_pdf <<~EOS, analyze: true
:source-highlighter: rouge
[source,sql]
----
SELECT
\tname,\t firstname,\t\tlastname
FROM
\tusers
WHERE
\tusername\t=\t'foobar'
----
EOS
lines = pdf.lines
(expect lines).to have_size 6
(expect lines).to include %(\u00a0 name, firstname, lastname)
(expect lines).to include %(\u00a0 username = 'foobar')
end
it 'should enable start_inline option for PHP by default' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,php]
----
echo "= (Gem::Version.new '2.1.0')
ref_funcname_text = (pdf.find_text 'cal_days_in_month')[0]
(expect ref_funcname_text).not_to be_nil
ref_year_text = (pdf.find_text '2019')[0]
(expect ref_year_text).not_to be_nil
funcname_text = (pdf.find_text 'cal_days_in_month')[1]
(expect funcname_text).not_to be_nil
year_text = (pdf.find_text '2019')[1]
(expect year_text).not_to be_nil
(expect funcname_text[:font_color]).not_to eql ref_funcname_text[:font_color]
(expect funcname_text[:font_name]).not_to eql ref_funcname_text[:font_name]
(expect year_text[:font_color]).to eql ref_year_text[:font_color]
(expect year_text[:font_name]).to eql ref_year_text[:font_name]
else
text = pdf.text
(expect text).to have_size 2
(expect text[0][:string]).to eql 'cal_days_in_month(CAL_GREGORIAN, 6, 2019)'
(expect text[0][:font_color]).to eql '333333'
(expect text[1][:string]).to eql 'cal_days_in_month(CAL_GREGORIAN, 6, 2019)'
(expect text[1][:font_color]).to eql '333333'
end
end
it 'should enable start_inline option for PHP if enabled by cgi-style option' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,php?start_inline=1]
----
echo "= (Gem::Version.new '2.1.0')
echo_text = pdf.find_unique_text 'echo'
(expect echo_text).not_to be_nil
# NOTE: the echo keyword should be highlighted
(expect echo_text[:font_color]).to eql '008800'
end
end
it 'should not enable the start_inline option for PHP if the mixed option is set and other cgi-style options specified' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source%mixed,php?foo=bar]
----
echo "= (Gem::Version.new '2.1.0')
prompt_text = pdf.find_unique_text '%'
(expect prompt_text).not_to be_nil
(expect prompt_text[:font_color]).to eql '555555'
end
end
it 'should use plain text lexer if language is not recognized and cgi-style options are present' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,foobar?start_inline=1]
----
puts "Hello, World!"
----
EOS
puts_text = (pdf.find_text 'puts')[0]
(expect puts_text).to be_nil
(expect pdf.text).to have_size 1
(expect pdf.text[0][:font_color]).to eql '333333'
end
it 'should use rouge style specified by rouge-style attribute', visual: true do
input = <<~'EOS'
:source-highlighter: rouge
:rouge-style: molokai
[source,js]
----
'use strict'
const TAG_ALL_RX = /<[^>]+>/g
module.exports = (html) => html && html.replace(TAG_ALL_RX, '')
----
EOS
to_file = to_pdf_file input, 'source-rouge-style.pdf'
(expect to_file).to visually_match 'source-rouge-style.pdf'
to_file = to_pdf_file input, 'source-rouge-style.pdf', attribute_overrides: { 'rouge-style' => (Rouge::Theme.find 'molokai').new }
(expect to_file).to visually_match 'source-rouge-style.pdf'
to_file = to_pdf_file input, 'source-rouge-style.pdf', attribute_overrides: { 'rouge-style' => (Rouge::Theme.find 'molokai') }
(expect to_file).to visually_match 'source-rouge-style.pdf'
end
it 'should disable highlighting instead of crashing if lexer fails to lex source' do
(expect do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,console]
----
$ cd application-name\bin\
----
EOS
source_lines = pdf.lines pdf.text {|it| it.font_name.start_with? 'mplus1mn-' }
(expect source_lines).not_to be_empty
(expect source_lines[0]).to start_with '$ cd'
end).not_to raise_exception
end
it 'should not crash if source-highlighter attribute is defined outside of document header' do
(expect do
pdf = to_pdf <<~'EOS', analyze: true
= Document Title
:source-highlighter: rouge
[source,ruby]
----
puts 'yo, world!'
----
EOS
source_text = pdf.find_unique_text font_name: 'mplus1mn-regular'
(expect source_text).not_to be_nil
(expect source_text[:string]).to start_with 'puts '
end).not_to raise_exception
end
it 'should apply bw style if specified' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
:rouge-style: bw
[source,ruby]
----
class Beer
attr_reader :style
end
----
EOS
beer_text = (pdf.find_text 'Beer')[0]
(expect beer_text).not_to be_nil
(expect beer_text[:font_name]).to eql 'mplus1mn-bold'
if (Gem::Version.new Rouge.version) >= (Gem::Version.new '3.4.0')
(expect beer_text[:font_color]).to eql '333333'
else
(expect beer_text[:font_color]).to eql 'BB0066'
end
end
it 'should allow token to be formatted in bold and italic' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
:rouge-style: github
[source,d]
----
int #line 6 "pkg/mod.d"
x; // this is now line 6 of file pkg/mod.d
----
EOS
line_text = pdf.find_unique_text %r/^#line 6 /
(expect line_text).not_to be_empty
(expect line_text[:font_name]).to eql 'mplus1mn-bold_italic'
end
it 'should allow token to add underline style to token', visual: true do
input = <<~'EOS'
:source-highlighter: rouge
[source,ruby]
----
class Beer
attr_reader :style
def drink
puts 'aaaaaaaaah'
end
end
----
EOS
# NOTE: convert to load Rouge
to_pdf input
rouge_style = Class.new Rouge::Theme.find 'molokai' do
style Rouge::Token::Tokens::Name::Class, fg: :green, bold: true, underline: true
style Rouge::Token::Tokens::Name::Function, fg: :green, underline: true
end
to_file = to_pdf_file input, 'source-rouge-underline-style.pdf', attribute_overrides: { 'rouge-style' => rouge_style }
(expect to_file).to visually_match 'source-rouge-underline-style.pdf' if (Gem::Version.new Rouge.version) >= (Gem::Version.new '2.1.0')
end
it 'should allow token to extend to width of block', visual: true do
input = <<~'EOS'
:source-highlighter: rouge
:rouge-style: github
[source,ruby]
----
puts 'string'
----
EOS
# NOTE: convert to load Rouge
to_pdf input
rouge_style = Class.new Rouge::Theme.find 'github' do
style Rouge::Token::Tokens::Literal::String::Single, fg: '#333333', bg: '#ff4dcd', extend: true, inline_block: true
end
pdf = to_pdf input, attribute_overrides: { 'rouge-style' => rouge_style }, analyze: :rect
rects = pdf.rectangles
(expect rects).to have_size 1
(expect rects[0][:width]).to be > 44
(expect rects[0][:height]).to be > 11
end
it 'should not crash if theme does not define style for Text token' do
input = <<~'EOS'
:source-highlighter: rouge
[source,ruby]
----
puts "Hello, World!"
----
EOS
# NOTE: convert to load Rouge
to_pdf input
rouge_style = Class.new Rouge::CSSTheme do
name 'foobar'
style Rouge::Token::Tokens::Literal::String, italic: true
end
pdf = to_pdf input, attribute_overrides: { 'rouge-style' => rouge_style }, analyze: true
hello_world_text = pdf.find_unique_text '"Hello, World!"'
(expect hello_world_text[:font_name]).to eql 'mplus1mn-italic'
end
it 'should expand color value for token' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
:rouge-style: colorful
[source,ruby]
----
class Type; end
----
EOS
pdf.text.each do |text|
(expect text[:font_color].length).to be 6
(expect text[:font_color].upcase).to eql text[:font_color]
end
classname_text = pdf.find_unique_text 'Type'
(expect ((Rouge::Theme.find 'colorful').new.style_for Rouge::Token::Tokens::Name::Class)[:fg]).to eql '#B06'
(expect classname_text[:font_color]).to eql 'BB0066'
end
it 'should draw background color around token', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-rouge-bg.pdf'
:source-highlighter: rouge
:rouge-style: pastie
[source,ruby]
----
type, name = ARGV
case type
when :hello
puts %(Hello, #{name}!)
when :goodbye
puts 'See ya, ' + name + '!'
end
----
EOS
(expect to_file).to visually_match 'source-rouge-bg.pdf' if (Gem::Version.new Rouge.version) >= (Gem::Version.new '2.1.0')
end
it 'should draw background color across whole line for line-oriented tokens', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-rouge-bg-line.pdf'
:source-highlighter: rouge
[source,diff]
----
--- /tmp/list1.txt
+++ /tmp/list2.txt
@@ -1,4 +1,4 @@
apples
-oranges
kiwis
carrots
+grapefruits
----
EOS
(expect to_file).to visually_match 'source-rouge-bg-line.pdf' if (Gem::Version.new Rouge.version) >= (Gem::Version.new '2.1.0')
end
it 'should not draw background color across whole line for line-oriented tokens if disabled in theme' do
input = <<~'EOS'
:source-highlighter: rouge
[source,diff]
----
-oranges
+grapefruits
----
EOS
# NOTE: convert to load Rouge
to_pdf input
rouge_style = Class.new Rouge::Theme.find 'asciidoctor_pdf_default' do
style Rouge::Token::Tokens::Generic::Deleted, fg: '#000000', bg: '#ffdddd', extend: false, inline_block: false
style Rouge::Token::Tokens::Generic::Inserted, fg: '#000000', bg: '#ddffdd', extend: false, inline_block: false
end
pdf = to_pdf input, attribute_overrides: { 'rouge-style' => rouge_style }, analyze: :rect
rects = pdf.rectangles
(expect rects).to have_size 2
(expect rects[0][:width]).to be < 100
# FIXME: the first token ends in a newline, so it gets coerced to an inline block
(expect rects[0][:height]).to eql 14.8
(expect rects[1][:height]).to be < 14.8
end
it 'should fall back to default line gap if line gap is not specified in theme', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-rouge-bg-line-no-gap.pdf', pdf_theme: { code_line_gap: nil }
:source-highlighter: rouge
[source,diff]
----
--- /tmp/list1.txt
+++ /tmp/list2.txt
@@ -1,4 +1,4 @@
apples
-oranges
kiwis
carrots
+grapefruits
----
EOS
(expect to_file).to visually_match 'source-rouge-bg-line-no-gap.pdf' if (Gem::Version.new Rouge.version) >= (Gem::Version.new '2.1.0')
end
it 'should add line numbers to start of line if linenums option is enabled' do
expected_lines = <<~'EOS'.split ?\n
1
2
3
4 https://example.org/home.html
5 2019-01-01T00:00:00.000Z
6
7
8 https://example.org/about.html
9 2019-01-01T00:00:00.000Z
10
11
EOS
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,xml,linenums]
----
https://example.org/home.html
2019-01-01T00:00:00.000Z
https://example.org/about.html
2019-01-01T00:00:00.000Z
----
EOS
(expect pdf.lines).to eql expected_lines
linenum_text = (pdf.find_text %r/^11 *$/)[0]
(expect linenum_text[:font_color]).to eql '888888'
end
it 'should continue to add line numbers after page split' do
source_lines = (1..55).map {|it| %(puts "Please come forward if your number is #{it}.") }
pdf = to_pdf <<~EOS, analyze: true
:source-highlighter: rouge
[source%linenums,ruby]
----
#{source_lines.join ?\n}
----
EOS
lines_after_split = pdf.lines pdf.find_text page_number: 2
(expect lines_after_split).not_to be_empty
(expect lines_after_split[0]).to eql '51 puts "Please come forward if your number is 51."'
end
it 'should honor start value for line numbering' do
expected_lines = <<~'EOS'.split ?\n
5 puts 'Hello, World!'
EOS
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,xml,linenums,start=5]
----
puts 'Hello, World!'
----
EOS
(expect pdf.lines).to eql expected_lines
end
it 'should coerce start value for line numbering to 1 if less than 1' do
expected_lines = <<~'EOS'.split ?\n
1 puts 'Hello, World!'
EOS
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,xml,linenums,start=0]
----
puts 'Hello, World!'
----
EOS
(expect pdf.lines).to eql expected_lines
end
it 'should not add line number to first line if source is empty' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source%linenums]
----
----
EOS
(expect pdf.text).to be_empty
end
it 'should not emit error if linenums are enabled and language is not set' do
(expect do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source%linenums]
----
fee
fi
fo
fum
----
EOS
(expect pdf.lines).to eql ['1 fee', '2 fi', '3 fo', '4 fum']
end).to not_log_message
end
it 'should preserve orphan callout on last line' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,yaml]
----
foo: 'bar'
key: 'value'
<1>
----
<1> End the file with a trailing newline
EOS
conum_texts = pdf.find_text '①'
(expect conum_texts).to have_size 2
end
it 'should use font color from style' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
:rouge-style: monokai
[source,text]
----
foo
bar
baz
----
EOS
pdf.text.each do |text|
(expect text[:font_color]).to eql 'F8F8F2'
end
end
it 'should highlight lines specified by highlight attribute on block', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-rouge-line-highlighting.pdf'
:source-highlighter: rouge
[source,c,highlight=4;7-8]
----
/**
* A program that prints "Hello, World!"
**/
#include
int main(void) {
printf("Hello, World!\n");
return 0;
}
----
EOS
(expect to_file).to visually_match 'source-rouge-line-highlighting.pdf'
end
it 'should highlight lines specified by highlight attribute on block when linenums are enabled', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-rouge-line-highlighting-with-linenums.pdf'
:source-highlighter: rouge
[source,c,linenums,highlight=4;7-8]
----
/**
* A program that prints "Hello, World!"
**/
#include
int main(void) {
printf("Hello, World!\n");
return 0;
}
----
EOS
(expect to_file).to visually_match 'source-rouge-line-highlighting-with-linenums.pdf'
end
it 'should interpret highlight lines relative to start value', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-rouge-line-highlighting-with-linenums-start.pdf'
:source-highlighter: rouge
[source,c,linenums,start=4,highlight=4;7-8]
----
#include
int main(void) {
printf("Hello, World!\n");
return 0;
}
----
EOS
(expect to_file).to visually_match 'source-rouge-line-highlighting-with-linenums-start.pdf'
end
it 'should preserve indentation when highlighting lines without linenums enabled', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-rouge-line-highlighting-indent.pdf'
:source-highlighter: rouge
[source,groovy,highlight=4-5]
----
ratpack {
handlers {
get {
render '''|Hello,
|World!'''.stripMargin()
}
}
}
----
EOS
(expect to_file).to visually_match 'source-rouge-line-highlighting-indent.pdf'
end
it 'should ignore highlight attribute if empty' do
pdf = to_pdf <<~'EOS', analyze: :rect
:source-highlighter: rouge
[source,ruby,linenums,highlight=]
----
puts "Hello, World!"
----
EOS
(expect pdf.rectangles).to be_empty
end
it 'should preserve indentation of highlighted line' do
input = <<~'EOS'
:source-highlighter: rouge
[source,text,highlight=1]
----
indented line
----
EOS
pdf = to_pdf input, analyze: true
(expect pdf.lines).to eql [%(\u00a0 indented line)]
pdf = to_pdf input, analyze: :rect
(expect pdf.rectangles).to have_size 1
end
it 'should highlight lines using custom color specified in theme', visual: true do
pdf_theme = { code_highlight_background_color: 'FFFF00' }
to_file = to_pdf_file <<~'EOS', 'source-rouge-highlight-background-color.pdf', pdf_theme: pdf_theme
:source-highlighter: rouge
[source,c,highlight=4]
----
/**
* A program that prints "Hello, World!"
**/
#include
int main(void) {
printf("Hello, World!\n");
return 0;
}
----
EOS
(expect to_file).to visually_match 'source-rouge-highlight-background-color.pdf'
end
it 'should indent wrapped line if line numbers are enabled' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,text,linenums]
----
Here we go again here we go again here we go again here we go again here we go again Here we go again
----
EOS
linenum_text = (pdf.find_text '1 ')[0]
(expect linenum_text[:x]).not_to be_nil
start_texts = pdf.find_text %r/^Here we go again/
(expect start_texts).to have_size 2
(expect start_texts[0][:x]).to eql start_texts[1][:x]
(expect start_texts[0][:x]).to be > linenum_text[:x]
indent_texts = pdf.find_text %r/\u00a0/
(expect indent_texts).to have_size 1
(expect indent_texts[0][:x]).to eql linenum_text[:x]
(expect indent_texts[0][:string]).to eql %(\u00a0 )
end
it 'should indent wrapped line if line numbers are enabled and block has an AFM font' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
:pdf-theme: base
[,text,linenums]
----
Here we go again here we go again here we go again here we go again here we go again Here we go again
----
EOS
linenum_text = (pdf.find_text '1 ')[0]
(expect linenum_text[:x]).not_to be_nil
start_texts = pdf.find_text %r/Here we go/
(expect start_texts).to have_size 2
(expect start_texts[0][:x]).to eql start_texts[1][:x]
(expect start_texts[0][:x]).to be > linenum_text[:x]
indent_texts = pdf.find_text %r/\u00a0/
(expect indent_texts).to have_size 1
(expect indent_texts[0][:x]).to eql linenum_text[:x]
(expect indent_texts[0][:string]).to eql %(\u00a0 )
end
it 'should highlight and indent wrapped line', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-rouge-highlight-wrapped-line.pdf'
:source-highlighter: rouge
[source,xml,linenums,highlight=1;3]
----
4.0.0
----
EOS
(expect to_file).to visually_match 'source-rouge-highlight-wrapped-line.pdf'
end
it 'should not apply syntax highlighting or borders and backgrounds in scratch document' do
scratch_pdf = nil
postprocessor_impl = proc do
process do |doc, output|
scratch_pdf = doc.converter.scratch
output
end
end
opts = { extension_registry: Asciidoctor::Extensions.create { postprocessor(&postprocessor_impl) } }
main_pdf = to_pdf <<~'EOS', (opts.merge analyze: true)
:source-highlighter: rouge
filler
[%unbreakable]
--
[source,ruby]
----
puts "Hello, World!"
----
--
EOS
main_pdf_text = main_pdf.text.reject {|it| it[:string] == 'filler' }
(expect main_pdf_text[0][:string]).to eql 'puts'
(expect main_pdf_text[0][:font_color]).not_to eql '333333'
scratch_pdf_output = scratch_pdf.render
scratch_pdf_text = (TextInspector.analyze scratch_pdf_output).text
(expect scratch_pdf_text[0][:string]).to eql 'puts "Hello, World!"'
(expect scratch_pdf_text[0][:font_color]).to eql '333333'
scratch_pdf_lines = (LineInspector.analyze scratch_pdf_output).lines
(expect scratch_pdf_lines).to be_empty
end
it 'should not leak patch for linenums if unbreakable block is split across pages' do
formatted_text_box_extensions_count = nil
extensions = proc do
postprocessor do
process do |_, output|
formatted_text_box_extensions_count = Prawn::Text::Formatted::Box.extensions.size
output
end
end
end
source_file = fixture_file 'TicTacToeGame.java'
pdf = to_pdf <<~EOS, extensions: extensions, enable_footer: true, analyze: true
:source-highlighter: rouge
before block
[%linenums%autofit%unbreakable,java]
----
include::#{source_file}[]
----
EOS
(expect (pdf.find_unique_text 'before block')[:page_number]).to be 1
(expect (pdf.find_unique_text 'package')[:page_number]).to be 1
(expect (pdf.find_unique_text 'package')[:font_color]).not_to be '333333'
(expect (pdf.find_unique_text %r/^\s*70\s*$/)[:page_number]).to be 2
(expect formatted_text_box_extensions_count).to be 0
end
it 'should break and wrap numbered line if text does not fit on a single line' do
input = <<~EOS
:source-highlighter: rouge
[%linenums,text]
----
y#{'o' * 100}
----
EOS
pdf = to_pdf input, analyze: true
(expect pdf.pages).to have_size 1
linenum_text = pdf.find_unique_text '1 '
(expect linenum_text).not_to be_nil
first_line_text = pdf.find_unique_text %r/^yo/
wrapped_text = pdf.find_unique_text %r/^ooo/
(expect linenum_text[:x]).to be < first_line_text[:x]
(expect linenum_text[:y]).to eql first_line_text[:y]
(expect linenum_text[:x]).to be < wrapped_text[:x]
(expect wrapped_text[:x]).to eql first_line_text[:x]
(expect linenum_text[:y]).to be > wrapped_text[:y]
lines = (to_pdf input, pdf_theme: { code_border_radius: 0, code_border_width: [1, 0] }, analyze: :line).lines
(expect (lines[0][:from][:y] - lines[1][:from][:y]).abs).to (be_within 2).of 50
end
it 'should break and wrap numbered line if indented text does not fit on a single line' do
input = <<~EOS
:source-highlighter: rouge
[%linenums,text]
----
before
#{' ' * 2}y#{'o' * 100}
after
----
EOS
pdf = to_pdf input, analyze: true
(expect pdf.pages).to have_size 1
text_lines = pdf.lines pdf.text
# FIXME: we lose the indentation on the second line, but that's true of plain listing blocks too
expected_lines = <<~EOS.chomp.split ?\n
1 before
2
\u00a0 yooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
\u00a0 ooooooooooooooooo
3 after
EOS
(expect text_lines).to eql expected_lines
end
it 'should break and wrap numbered line if text wraps but still does not fit on a single line' do
input = <<~EOS
:source-highlighter: rouge
[%linenums,text]
----
one
two and then s#{'o' * 100}me
three
----
EOS
pdf = to_pdf input, analyze: true
(expect pdf.pages).to have_size 1
text_lines = pdf.lines pdf.text
expected_lines = <<~EOS.chomp.split ?\n
1 one
2 two and then
\u00a0 sooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
\u00a0 ooooooooooooooooome
3 three
EOS
(expect text_lines).to eql expected_lines
end
it 'should not apply syntax highlighting if specialchars sub is disabled' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,ruby,subs=-specialchars]
----
puts "Hello, World!"
----
EOS
text = pdf.text
(expect text).to have_size 1
(expect text[0][:string]).to eql 'puts "Hello, World!"'
(expect text[0][:font_color]).to eql '333333'
end
it 'should not apply syntax highlighting in scratch document if specialchars sub is disabled' do
scratch_pdf = nil
postprocessor_impl = proc do
process do |doc, output|
scratch_pdf = doc.converter.scratch
output
end
end
opts = { extension_registry: Asciidoctor::Extensions.create { postprocessor(&postprocessor_impl) } }
pdf = to_pdf <<~'EOS', (opts.merge analyze: true)
:source-highlighter: rouge
[%unbreakable]
--
[source,ruby,subs=-specialchars]
----
puts "Hello, World!"
----
--
EOS
[pdf.text, (TextInspector.analyze scratch_pdf.render).text].each do |text|
(expect text[0][:string]).to eql 'puts "Hello, World!"'
(expect text[0][:font_color]).to eql '333333'
end
end
it 'should remove bare HTML tags added by substitutions' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[,ruby,subs="+quotes,+macros"]
----
require %(https://asciidoctor.org[asciidoctor])
Asciidoctor._convert_file_ 'README.adoc', *safe: :safe*
----
EOS
lines = pdf.lines pdf.text
(expect lines).to eql ['require %(asciidoctor)', %(Asciidoctor.convert_file 'README.adoc', safe: :safe)]
require_text = pdf.find_unique_text 'require'
(expect require_text[:font_color]).not_to eql '333333'
end
it 'should substitute attribute references if attributes substitution is enabled' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
:converter-library: asciidoctor-pdf
:backend-value: :pdf
[,ruby,subs=attributes+]
----
require '{converter-library}'
Asciidoctor.convert_file 'README.adoc', safe: :safe, backend: {backend-value}
----
EOS
lines = pdf.lines pdf.text
(expect lines).to eql [%(require 'asciidoctor-pdf'), %(Asciidoctor.convert_file 'README.adoc', safe: :safe, backend: :pdf)]
require_text = pdf.find_unique_text 'require'
(expect require_text[:font_color]).not_to eql '333333'
end
end if gem_available? 'rouge'
context 'CodeRay' do
it 'should highlight source using CodeRay if source-highlighter is coderay' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: coderay
[source,ruby]
----
puts 'Hello, CodeRay!'
----
EOS
hello_text = (pdf.find_text 'Hello, CodeRay!')[0]
(expect hello_text).not_to be_nil
(expect hello_text[:font_color]).to eql 'CC3300'
(expect hello_text[:font_name]).to eql 'mplus1mn-regular'
end
it 'should fallback to text language if language is not recognized' do
(expect do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: coderay
[,scala]
----
val l = List(1,2,3,4)
l.foreach {i => println(i)}
----
EOS
expected_color = '333333'
(expect pdf.text.map {|it| it[:font_color] }.uniq).to eql [expected_color]
end).not_to raise_exception
end
it 'should not crash if token text is nil' do
(expect do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: coderay
[source,sass]
----
$icon-font-path: "node_modules/package-name/icon-fonts/";
body {
background: #fafafa;
}
----
EOS
closing_bracket_text = pdf.find_unique_text '}'
(expect closing_bracket_text[:font_color]).to eql 'CC3300'
end).not_to raise_exception
end
it 'should use sub-language if language starts with html+' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: coderay
[source,html+js]
----
document.addEventListener('load', function () { console.log('page is loaded!') })
----
EOS
message_text = (pdf.find_text 'page is loaded!')[0]
(expect message_text).not_to be_nil
(expect message_text[:font_color]).to eql 'CC3300'
end
it 'should fall back to text if language does not have valid characters' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: coderay
[source,?!?]
----
,[.,]
----
EOS
text = (pdf.find_text ',[.,]')[0]
(expect text[:font_color]).to eql '333333'
end
it 'should not crash if source-highlighter attribute is defined outside of document header' do
(expect do
to_pdf <<~'EOS'
= Document Title
:source-highlighter: coderay
[source,ruby]
----
puts 'yo, world!'
----
EOS
end).not_to raise_exception
end
it 'should add indentation guards at start of line that begins with space to preserve indentation' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: coderay
[source,yaml]
----
category:
hash:
key: "value"
----
EOS
(expect pdf.lines).to eql ['category:', %(\u00a0 hash:), %(\u00a0 key: "value")]
end
it 'should expand tabs to preserve indentation' do
pdf = to_pdf <<~EOS, analyze: true
:source-highlighter: coderay
[source,c]
----
int main() {
\tevent_loop();
\treturn 0;
}
----
EOS
lines = pdf.lines
(expect lines).to have_size 4
(expect lines[1]).to eql %(\u00a0 event_loop();)
(expect lines[2]).to eql %(\u00a0 return 0;)
end
it 'should extract conums so they do not interfere with syntax highlighting' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: coderay
[source,xml]
----
attr="value">
content
----
EOS
attr_name_text = (pdf.find_text 'attr')[0]
(expect attr_name_text).not_to be_nil
(expect attr_name_text[:font_color]).to eql '4F9FCF'
(expect (pdf.find_text '①')[0]).not_to be_nil
end
end
context 'Pygments' do
it 'should highlight source using Pygments if source-highlighter is pygments' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
[source,ruby]
----
puts "Hello, Pygments!"
----
EOS
hello_text = (pdf.find_text '"Hello, Pygments!"')[0]
(expect hello_text).not_to be_nil
(expect hello_text[:font_color]).to eql 'DD2200'
(expect hello_text[:font_name]).to eql 'mplus1mn-regular'
end
it 'should display encoded source without highlighting if lexer fails to return a value' do
input = <<~'EOS'
:source-highlighter: pygments
[source,xml]
----
&
----
EOS
# warm up pygments
pdf = to_pdf input, analyze: true
(expect pdf.text).to have_size 3
xml_lexer = Pygments::Lexer.find_by_alias 'xml'
# emulate highlight returning nil
class << xml_lexer
alias_method :_highlight, :highlight
def highlight *_args; end
end
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
[source,xml]
----
&
----
EOS
(expect pdf.text).to have_size 1
source_text = pdf.text[0]
(expect source_text[:string]).to eql '&'
(expect source_text[:font_color]).to eql '333333'
ensure
class << xml_lexer
undef_method :highlight
alias_method :highlight, :_highlight
end
end
it 'should not crash when adding indentation guards' do
(expect do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
[source,yaml]
----
category:
hash:
key: "value"
----
EOS
(expect pdf.find_text %r/: ?/).to have_size 3
lines = pdf.lines
(expect lines).to have_size 3
(expect lines[0]).to eql 'category:'
(expect lines[1]).to eql %(\u00a0 hash:)
(expect lines[2]).to eql %(\u00a0 key: "value")
(expect pdf.find_text '"value"').to have_size 1
end).not_to raise_exception
end
it 'should expand tabs to preserve indentation' do
pdf = to_pdf <<~EOS, analyze: true
:source-highlighter: pygments
[source,c]
----
int main() {
\tevent_loop();
\treturn 0;
}
----
EOS
lines = pdf.lines
(expect lines).to have_size 4
(expect lines[1]).to eql %(\u00a0 event_loop();)
(expect lines[2]).to eql %(\u00a0 return 0;)
end
it 'should use plain text lexer if language is not recognized' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
[source,foobar]
----
puts "Hello, World!"
----
EOS
puts_text = (pdf.find_text 'puts')[0]
(expect puts_text).to be_nil
(expect pdf.text).to have_size 1
(expect pdf.text[0][:font_color]).to eql '333333'
end
it 'should enable start_inline option for PHP by default' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
[source,php]
----
echo "
2
3
4 https://example.org/home.html
5 2019-01-01T00:00:00.000Z
6
7
8 https://example.org/about.html
9 2019-01-01T00:00:00.000Z
10
11
EOS
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
[source,xml,linenums]
----
https://example.org/home.html
2019-01-01T00:00:00.000Z
https://example.org/about.html
2019-01-01T00:00:00.000Z
----
EOS
(expect pdf.lines).to eql expected_lines
end).not_to raise_exception
end
it 'should honor start value for line numbering' do
expected_lines = <<~'EOS'.split ?\n
5 puts 'Hello, World!'
EOS
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
[source,xml,linenums,start=5]
----
puts 'Hello, World!'
----
EOS
(expect pdf.lines).to eql expected_lines
end
it 'should preserve space before callout on last line' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
[source,yaml]
----
foo: 'bar'
key: 'value' #<1>
----
<1> key-value pair
EOS
text = pdf.text
conum_idx = text.index {|it| it[:string] == '①' }
(expect text[conum_idx - 1][:string]).to eql ' '
(expect text[conum_idx - 2][:string]).to eql '\'value\''
end
it 'should support background color on highlighted tokens', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-pygments-token-background-color.pdf'
:source-highlighter: pygments
:pygments-style: murphy
[source,ruby]
----
# Matches a hex color value like #FF0000
if /^#[a-fA-F0-9]{6}$/.match? color
puts 'hex color'
end
----
EOS
(expect to_file).to visually_match 'source-pygments-token-background-color.pdf'
end
it 'should use background color from style', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-pygments-background-color.pdf', pdf_theme: { code_background_color: 'fafafa' }
:source-highlighter: pygments
:pygments-style: monokai
.Ruby
[source,ruby]
----
if /^#[a-fA-F0-9]{6}$/.match? color
puts 'hex color'
end
----
.JavaScript
[source,js]
----
'use strict'
const TAG_ALL_RX = /<[^>]+>/g
module.exports = (html) => html && html.replace(TAG_ALL_RX, '')
----
EOS
(expect to_file).to visually_match 'source-pygments-background-color.pdf'
end
it 'should use font color from style' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
:pygments-style: monokai
[source,text]
----
foo
bar
baz
----
EOS
pdf.text.each do |text|
(expect text[:font_color]).to eql 'F8F8F2'
end
end
it 'should ignore highlight attribute if empty' do
pdf = to_pdf <<~'EOS', analyze: :rect
:source-highlighter: pygments
:pygments-style: tango
[source,ruby,linenums,highlight=]
----
puts "Hello, World!"
----
EOS
(expect pdf.rectangles).to be_empty
end
it 'should fall back to pastie style if style is not recognized' do
(expect do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
:pygments-style: not-recognized
[source,ruby]
----
# Matches a hex color value like #FF0000
if /^#[a-fA-F0-9]{6}$/.match? color
puts 'hex color'
end
----
EOS
comment_text = pdf.find_unique_text %r/^#/
(expect comment_text[:font_color]).to eql '888888'
rx_text = pdf.find_unique_text %r/^\/\^/
(expect rx_text[:font_color]).to eql '008800'
end).not_to raise_exception
end
it 'should highlight selected lines but not the line numbers', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-pygments-line-highlighting.pdf'
:source-highlighter: pygments
[source,groovy,linenums,highlight=7-9]
----
package com.example
import static ratpack.groovy.Groovy.ratpack
ratpack {
handlers {
get {
render "Hello, World!"
}
}
}
----
EOS
(expect to_file).to visually_match 'source-pygments-line-highlighting.pdf'
end
it 'should not add line number to first line if source is empty' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
[source%linenums]
----
----
EOS
(expect pdf.text).to be_empty
end
it 'should not emit error if linenums are enabled and language is not set' do
(expect do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
[source%linenums]
----
fee
fi
fo
fum
----
EOS
(expect pdf.lines).to eql ['1 fee', '2 fi', '3 fo', '4 fum']
end).to not_log_message
end
it 'should indent wrapped line if line numbers are enabled' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: pygments
[source,text,linenums]
----
Here we go again here we go again here we go again here we go again here we go again Here we go again
----
EOS
linenum_text = (pdf.find_text '1 ')[0]
(expect linenum_text[:x]).not_to be_nil
start_texts = pdf.find_text %r/^Here we go again/
(expect start_texts).to have_size 2
(expect start_texts[0][:x]).to eql start_texts[1][:x]
(expect start_texts[0][:x]).to be > linenum_text[:x]
end
it 'should highlight and indent wrapped line', visual: true do
to_file = to_pdf_file <<~'EOS', 'source-pygments-highlight-wrapped-line.pdf'
:source-highlighter: pygments
[source,xml,linenums,highlight=1;3]
----
4.0.0
----
EOS
(expect to_file).to visually_match 'source-pygments-highlight-wrapped-line.pdf'
end
it 'should guard inner indents' do
pdf = to_pdf <<~EOS, analyze: true
:source-highlighter: pygments
[source,text]
----
lead space
flush
lead space
----
EOS
(expect pdf.lines).to eql [%(\u00a0 lead space), 'flush', %(\u00a0 lead space)]
end
it 'should ignore fragment if empty' do
pdf = to_pdf <<~EOS, analyze: true
:source-highlighter: pygments
[source,ruby]
----
<1>
----
EOS
(expect pdf.lines).to eql ['①']
end
end if gem_available? 'pygments.rb'
context 'Unsupported' do
it 'should apply specialcharacters substitution and indentation guards for client-side syntax highlighter' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: highlight.js
[source,xml]
----
content
----
EOS
(expect pdf.lines).to eql ['', %(\u00a0 content), '']
(expect pdf.text.map {|it| it[:font_color] }.uniq).to eql ['333333']
end
it 'should apply specialcharacters substitution and indentation guards if syntax highlighter is unsupported' do
Class.new Asciidoctor::SyntaxHighlighter::Base do
register_for :foobar
def highlight?
true
end
end
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: foobar
[source,xml]
----
content
----
EOS
(expect pdf.lines).to eql ['', %(\u00a0 content), '']
(expect pdf.text.map {|it| it[:font_color] }.uniq).to eql ['333333']
end
end
context 'Callouts' do
it 'should allow callout to be escaped' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,ruby]
----
source = %(before
\<1>
after)
----
EOS
(expect pdf.lines).to include '<1>'
(expect pdf.find_text '①').to be_empty
end
it 'should not replace callouts if callouts sub is not present' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,ruby,subs=-callouts]
----
source = %(before
not a conum <1>
after)
----
EOS
(expect pdf.lines).to include 'not a conum <1>'
(expect pdf.find_text '①').to be_empty
end
it 'should inherit font color if not set in theme when source highlighter is enabled' do
pdf = to_pdf <<~'EOS', pdf_theme: { code_font_color: '111111', conum_font_color: nil }, analyze: true
:source-highlighter: rouge
[source,ruby]
----
puts 'Hello, World' <1>
----
<1> Just a programming saying hi.
EOS
conum_texts = pdf.find_text %r/①/
(expect conum_texts).to have_size 2
(expect conum_texts[0][:font_color]).to eql '111111'
(expect conum_texts[1][:font_color]).to eql '333333'
end
it 'should inherit font color if not set in theme when source highlighter is not enabled' do
pdf = to_pdf <<~'EOS', pdf_theme: { code_font_color: '111111', conum_font_color: nil }, analyze: true
[source,ruby]
----
puts 'Hello, World' <1>
----
<1> Just a programming saying hi.
EOS
conum_texts = pdf.find_text %r/①/
(expect conum_texts).to have_size 2
(expect conum_texts[0][:font_color]).to eql '111111'
(expect conum_texts[1][:font_color]).to eql '333333'
end
it 'should process a sequence of two or more callouts when not separated by spaces' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,java]
----
public interface Person {
String getName(); // <1><2>
String getDob();
int getAge(); // <3><4><5>
}
----
EOS
lines = pdf.lines
(expect lines[1]).to end_with '; ① ②'
(expect lines[3]).to end_with '; ③ ④ ⑤'
end
it 'should process a sequence of two or more callouts when separated by spaces' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,java]
----
public interface Person {
String getName(); // <1> <2>
String getDob();
int getAge(); // <3> <4> <5>
}
----
EOS
lines = pdf.lines
(expect lines[1]).to end_with '; ① ②'
(expect lines[3]).to end_with '; ③ ④ ⑤'
end
it 'should honor font family set on conum category in theme for conum in source block' do
pdf = to_pdf <<~'EOS', pdf_theme: { code_font_family: 'Courier' }, analyze: true
:source-highlighter: rouge
[source,java]
----
public interface Person {
String getName(); <1>
String getDob(); <2>
int getAge(); <3>
}
----
EOS
lines = pdf.lines
(expect lines[1]).to end_with '; ①'
(expect lines[2]).to end_with '; ②'
(expect lines[3]).to end_with '; ③'
conum_text = (pdf.find_text '①')[0]
(expect conum_text[:font_name]).not_to eql 'Courier'
end
it 'should substitute autonumber callouts with circled numbers when using rouge as syntax highlighter' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,java]
----
public interface Person {
String getName(); // <.>
String getDob(); // <.>
int getAge(); // <.>
}
----
EOS
lines = pdf.lines
(expect lines[1]).to end_with '; ①'
(expect lines[2]).to end_with '; ②'
(expect lines[3]).to end_with '; ③'
end
it 'should process multiple autonumber callouts on a single line when using rouge as syntax highlighter' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,java]
----
public interface Person {
String getName(); // <.>
String getDob(); // <.>
int getAge(); // <.> <.>
}
----
EOS
lines = pdf.lines
(expect lines[1]).to end_with '; ①'
(expect lines[2]).to end_with '; ②'
(expect lines[3]).to end_with '; ③ ④'
end
it 'should preserve space before callout on final line' do
['rouge', (gem_available? 'pygments.rb') ? 'pygments' : nil].compact.each do |highlighter|
pdf = to_pdf <<~'EOS', attribute_overrides: { 'source-highlighter' => highlighter }, analyze: true
[source,java]
----
public interface Person {
String getName();
} <1>
----
<1> End class definition
EOS
lines = pdf.lines
(expect lines).to include '} ①'
end
end
it 'should hide spaces in front of conum from source highlighter' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[source,apache]
----
AllowOverride None <1>
Require all granted
----
<1> Cannot be overridden by .htaccess
EOS
none_text = (pdf.find_text 'None')[0]
(expect none_text).not_to be_nil
(expect none_text[:font_color]).to eql 'AA6600'
end
it 'should preserve callouts if custom subs are used on code block when source highlighter is enabled' do
pdf = to_pdf <<~'EOS', analyze: true
:source-highlighter: rouge
[,ruby,subs=+quotes]
----
puts "*Hello, World!*" <1>
----
<1> Welcome the world to the show.
EOS
lines = pdf.lines
(expect lines).to have_size 2
(expect lines[0]).to eql 'puts "Hello, World!" ①'
(expect lines[1]).to eql '① Welcome the world to the show.'
msg = pdf.find_unique_text '"Hello, World!"'
(expect msg[:font_name]).to eql 'mplus1mn-regular'
end
end if gem_available? 'rouge'
context 'Label' do
it 'should add label to code block if language is specified' do
source_file = doc_file 'modules/extend/examples/pdf-converter-source-language-label.rb'
source_lines = (File.readlines source_file).select {|l| l == ?\n || (l.start_with? ' ') }
ext_class = create_class Asciidoctor::Converter.for 'pdf'
backend = %(pdf#{ext_class.object_id})
source_lines[0] = %( register_for '#{backend}'\n)
ext_class.class_eval source_lines.join, source_file
pdf = to_pdf <<~'EOS', backend: backend, analyze: true
:source-highlighter: coderay
[,ruby]
----
lines = (File.readlines ARGV[0]).each_with_object [] do |ln, acc|
acc << ln unless (ln = ln.rstrip).empty? || (ln.start_with? '#')
end
puts lines * ?\n
----
EOS
midpoint = pdf.pages[0][:size][0] * 0.5
reference_text = (pdf.find_text 'lines')[0]
label_text = pdf.find_unique_text 'RUBY'
(expect label_text).not_to be_nil
(expect label_text[:y]).to eql reference_text[:y]
(expect label_text[:x]).to be > midpoint
(expect label_text[:font_size]).to eql reference_text[:font_size]
end
it 'should add label to code block in second column if language is specified' do
source_file = doc_file 'modules/extend/examples/pdf-converter-source-language-label.rb'
source_lines = (File.readlines source_file).select {|l| l == ?\n || (l.start_with? ' ') }
ext_class = create_class Asciidoctor::Converter.for 'pdf'
backend = %(pdf#{ext_class.object_id})
source_lines[0] = %( register_for '#{backend}'\n)
ext_class.class_eval source_lines.join, source_file
pdf_theme = { page_columns: 2, page_column_gap: 12 }
pdf = to_pdf <<~'EOS', backend: backend, pdf_theme: pdf_theme, analyze: true
:source-highlighter: coderay
image::tall-spacer.png[pdfwidth=5]
[%unbreakable,ruby]
----
require 'asciidoctor-pdf'
Asciidoctor.convert_file 'README.adoc',
backend: 'pdf', safe: :safe
----
EOS
midpoint = pdf.pages[0][:size][0] * 0.5
reference_text = (pdf.find_text 'require')[0]
label_text = pdf.find_unique_text 'RUBY'
(expect label_text).not_to be_nil
(expect label_text[:y]).to eql reference_text[:y]
(expect label_text[:x]).to be > midpoint
(expect label_text[:font_size]).to eql reference_text[:font_size]
end
end
end