caracal 1.4.1 → 1.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07032410054f6b6e5b8d640aa391c24382eda9461832b7f3fb0fe2bb9203164f
4
- data.tar.gz: 780ae708802f68185d556d6ec0dc28b5fdb86adefc6043ec18bf32027440278b
3
+ metadata.gz: 3357d1a61526623b3acb0ead4e0b990ce1c70026ea321ba014415947380daec9
4
+ data.tar.gz: 5be1b9e7bec559c1199c2c17befe1238c3ab522c1ef1fdb3a49a58e3b102c67f
5
5
  SHA512:
6
- metadata.gz: 136540cd7409584e274462d2f43b7b21f4533a4351611ea41f0db03fd6282a366d01c0a5d1f6b4d05d7674d9bdd9f939b536ba569189407abcfe253e8fc524c8
7
- data.tar.gz: d9c61a1f362f43625aaa3160639f903ebd003df993dc8e78c17e456bb1a1205aef94b5e1dc8cdf9b69136ba71786bb2eeee03f8e26ae35c1862ea329d1fe9162
6
+ metadata.gz: ad771cd941a199997acf1138bd1d426e5f2a4d1d5feb2065366a85de09a5f31295f9eee3a97b7264732e27b09706c722e4532f0a2164f73c00d3b3543623eddc
7
+ data.tar.gz: d89fe871c0f5569292adfd8ec6a5c044bf1096ecb95e711b2c55a9a4aa1324317d757f5ab8057d20582ff0ccd3c5b6f150c5b5a25a3a4aed81634a495ce32ad2
@@ -0,0 +1,28 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ name: Ruby ${{ matrix.ruby }} / rubyzip ${{ matrix.rubyzip }}
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ ruby: ['3.1', '3.2', '3.3', '3.4', '4.0']
19
+ rubyzip: ['~> 1.3', '~> 3.4']
20
+ env:
21
+ RUBYZIP_VERSION: ${{ matrix.rubyzip }}
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: ruby/setup-ruby@v1
25
+ with:
26
+ ruby-version: ${{ matrix.ruby }}
27
+ bundler-cache: true
28
+ - run: bundle exec rspec
data/CHANGELOG.md CHANGED
@@ -1,10 +1,37 @@
1
+ #### v1.5.0
2
+
3
+ * Enhancements
4
+ * Added the `keep_next` paragraph property, which keeps a paragraph on the same page as the one following it. (@benjaminketron)
5
+ * Added the `header_rows` table method, which repeats the leading rows of a table after each page break. (@benjaminketron)
6
+ * Added the `cant_split` and `cant_split?` table methods, which keep a row from splitting across a page break. (@benjaminketron)
7
+ * Added the `column_widths` table method, which sets the table grid explicitly instead of deriving it from the first row. (@martinsp)
8
+ * Widened the rubyzip dependency to `>= 1.1.6, < 4.0`, allowing rubyzip 3.x. (@acwertman)
9
+ * Added continuous integration across Ruby 3.1-4.0 and both supported rubyzip lines. (@dlauer)
10
+ * Declared `required_ruby_version >= 3.1`. (@dlauer)
11
+ * Documented the trust contract for image and iframe sources in a new Security section. (@dlauer)
12
+
13
+ * Bug Fixes
14
+ * Images and iframes are no longer loaded through `Kernel#open`, which executed a target beginning with `|` as a shell command, stopped fetching URLs entirely on Ruby 3.0 and later, and read in text mode on Windows, corrupting image data. Both are now read in binary, with `URI.open` for http(s) targets and `File.binread` for local paths. (@LItterBoy-GB, @dlauer)
15
+ * Removed control characters that XML does not permit from text, links, table cells, page number labels and custom properties. A single such character made Word reject the entire document. (@dlauer)
16
+ * Image part names and content types are now derived from the image data when it is supplied, rather than from the URL. This fixes presigned URLs, extensionless URLs and images carried in embedded documents. Registered content types for bmp, tiff and svg. (@dlauer)
17
+ * `iframe` now works inside table cells. (@catmando, @dlauer)
18
+ * Raised the rubyzip floor past versions whose `write_buffer` required an argument, which raised `ArgumentError` on save. (@acwertman)
19
+ * Corrected the documented units for table border and rule spacing, which are points rather than twips or eighths of a point. (@dlauer)
20
+
21
+
22
+ #### v1.4.1
23
+
24
+ * Bug Fixes
25
+ * Corrected rowspan logic to handle all cases. (@martinsp)
26
+
27
+
1
28
  #### v1.4.0
2
29
 
3
30
  * Enhancements
4
31
  * Changed `file_name` method to accept absolute and relative paths. (@jdugan)
5
32
  * Added instance save method so documents can be created via commands instead of blocks, if desired. (@jdugan)
6
33
  * Removed extra paragraph tag inserted after tables; added logic to ensure final tag of table cells is always a paragraph. (@jdugan)
7
-
34
+
8
35
 
9
36
  #### v1.3.0
10
37
 
data/Gemfile CHANGED
@@ -1,3 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ # lets CI test against specific rubyzip versions
6
+ gem 'rubyzip', ENV['RUBYZIP_VERSION'] if ENV['RUBYZIP_VERSION']
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Trade Informatics, Inc.
1
+ Copyright (c) 2014 Urvin LLC
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -11,7 +11,7 @@ Caracal is not a magical HTML to Word translator. Instead, it is a markup langua
11
11
 
12
12
  Or, said differently, if you use [Prawn](https://github.com/prawnpdf/prawn) for PDF generation, you'll probably like Caracal. Only you'll probably like it better. :)
13
13
 
14
- Please see the [caracal-example](https://github.com/trade-informatics/caracal-example) repository for
14
+ Please see the [caracal-example](https://github.com/urvin-compliance/caracal-example) repository for
15
15
  a working demonstration of the library's capabilities.
16
16
 
17
17
 
@@ -207,6 +207,19 @@ All Caracal models perform basic validations on their attributes, but this is, w
207
207
  In forthcoming versions of Caracal, we'll be looking to expand the `InvalidModelError` class to provide broader error reporting abilities across the entire library.
208
208
 
209
209
 
210
+ ## Security
211
+
212
+ Caracal reads whatever path or URL you hand it. Both `img` and `iframe` accept a local filesystem path or an http(s) URL, and Caracal reads or fetches it while the document renders.
213
+
214
+ **Treat those values as trusted input.** If a path or URL can be influenced by an end user, validate it before passing it to Caracal:
215
+
216
+ * **Local paths** are read with the permissions of your application's process. Any file that process can read can be embedded in the generated document, so `docx.img '/etc/passwd'` embeds the contents of that file.
217
+ * **URLs** are fetched from your application's network position, and redirects are followed. A user-supplied URL can therefore reach internal services, `localhost`, or cloud metadata endpoints that are not otherwise reachable from outside.
218
+ * **Documents embedded with `iframe`** are parsed, and their markup is copied into your output. Only embed documents you trust. See [IFrames](#iframes).
219
+
220
+ Caracal deliberately does not restrict paths, hosts, or directories, because it cannot know what is legitimate for your application. That policy belongs in your code: an allowlist of permitted directories or hosts, or simply never passing user-supplied values through to Caracal.
221
+
222
+
210
223
  ## Installation
211
224
 
212
225
  Add this line to your application's Gemfile:
@@ -415,6 +428,7 @@ docx.p 'Sample text.' do
415
428
  bgcolor 'cccccc' # sets the background color.
416
429
  highlight_color 'yellow' # sets the highlight color. only accepts OOXML enumerations. see http://www.datypic.com/sc/ooxml/t-w_ST_HighlightColor.html.
417
430
  vertical_align 'superscript' # sets the vertical alignment.
431
+ keep_next false # sets whether or not to keep this paragraph with the next paragraph when separated by page break.
418
432
  end
419
433
  ```
420
434
 
@@ -505,7 +519,7 @@ docx.hr do
505
519
  color '333333' # sets the color of the line. defaults to auto.
506
520
  line :double # sets the line style (single or double). defaults to single.
507
521
  size 8 # sets the thickness of the line. units in 1/8 points. defaults to 4.
508
- spacing 4 # sets the spacing around the line. units in 1/8 points. defaults to 1.
522
+ spacing 4 # sets the space between the line and the surrounding text. units in points. defaults to 1.
509
523
  end
510
524
  ```
511
525
 
@@ -625,6 +639,8 @@ is a bit hacky, but it allows the library to key the image more effectively and
625
639
  Caracal needs a file extension to apply to the renamed media file. This seemed
626
640
  the simplest solution to both problems.*
627
641
 
642
+ *If the URL or path can come from user input, read [Security](#security) first. Caracal will read any local file your process can read, and will fetch any URL you give it.*
643
+
628
644
 
629
645
  ### Tables
630
646
 
@@ -632,7 +648,7 @@ the simplest solution to both problems.*
632
648
  Release v1.4.0 deprecated the behaviour of automatically adding an empty paragraph tag after every table. If you are upgrading from an older version of the library, you will need to control such spacing in your own code.
633
649
  ```
634
650
 
635
- Tables can be added using the `table` method. The method accepts several optional paramters to control the layout and style of the table cells.
651
+ Tables can be added using the `table` method. The method accepts several optional parameters to control the layout and style of the table cells.
636
652
 
637
653
  The `table` command accepts data in the form of a two-dimensional arrays. This corresponds to rows and column cells within those rows. Each array item can be a string, a Hash of options, a Proc (which will be passed as a block), or a `TableCellModel`. The command will normalize all array contents into a two-dimensional array of `TableCellModel` instances.
638
654
 
@@ -640,8 +656,20 @@ The `table` command accepts data in the form of a two-dimensional arrays. This c
640
656
  docx.table [['Header 1','Header 2'],['Cell 1', 'Cell 2']] do
641
657
  border_color '666666' # sets the border color. defaults to 'auto'.
642
658
  border_line :single # sets the border style. defaults to :single. see OOXML docs for details.
643
- border_size 4 # sets the border width. defaults to 0. units in twips.
644
- border_spacing 4 # sets the spacing around the border. defaults to 0. units in twips.
659
+ border_size 4 # sets the border width. defaults to 0. units in 1/8 points.
660
+ border_spacing 4 # sets the space between the border and the cell text. defaults to 0. units in points.
661
+ column_widths [2400, 1200]
662
+ # sets an explicit width for each column in the table grid. units in twips.
663
+ # defaults to the widths of the cells in the first row.
664
+ end
665
+ ```
666
+
667
+ By default, the table grid is built from the cells in the first row. When that row contains a cell with a `colspan`, the grid can't be derived from it correctly and every column ends up the same width. `column_widths` sets the grid explicitly instead, taking one width per column.
668
+
669
+ ```ruby
670
+ docx.table [['Spans two columns'], ['Cell 1', 'Cell 2']] do
671
+ cell_style rows[0][0], colspan: 2
672
+ column_widths [2400, 1200]
645
673
  end
646
674
  ```
647
675
 
@@ -683,6 +711,31 @@ end
683
711
 
684
712
  *Note: content of cells 21 and 24 will disappear*
685
713
 
714
+ Table rows can be marked as header rows using the `header_rows` method which takes a number indicating how many rows from the top of the table should be marked as such. This has the effect of repeating these rows after each page break.
715
+
716
+ ```ruby
717
+ docx.table [['Header 1', 'Header 2'],['Cell 1', 'Cell 2']] do
718
+ header_rows 1
719
+ end
720
+ ```
721
+
722
+ Row can be marked as can't split rows that re-position themselves at the top of the next if they would have otherwise split across a page break.
723
+
724
+ ```
725
+ docx.table [['11', '22'], ['14', '25'], ['64', '23']] do
726
+ cant_split rows[1]
727
+ end
728
+ ```
729
+
730
+ or
731
+
732
+ ```
733
+ docx.table [['11', '22'], ['14', '25'], ['64', '23']] do
734
+ cant_split rows[1...-1]
735
+ end
736
+ ```
737
+
738
+
686
739
  ### Table Cells
687
740
 
688
741
  If your table contains more complex data (multiple paragraphs, images, lists, etc.), you will probably want to instantiate your `TableCellModel` instances directly. With the exception of page breaks, table cells can contain anything the document can contain, including another table.
@@ -760,17 +813,19 @@ will not operate as expected in the parent Caracal document.
760
813
 
761
814
  Again, this feature is considered experimental. Use at your own risk/discretion.
762
815
 
816
+ **Only embed documents you trust.** Caracal parses the file you supply and copies its markup into your output, so the embedded document controls part of the result. Caracal also places no limit on how much data it will decompress while reading the file, so a small crafted document can expand to an arbitrarily large one in memory. Documents uploaded by untrusted users should not be passed to `iframe` without your own validation. See [Security](#security).
817
+
763
818
 
764
819
  ## Template Rendering
765
820
 
766
821
  Caracal includes [Tilt](https://github.com/rtomayko/tilt) integration to facilitate its inclusion in other frameworks.
767
822
 
768
- Rails integration can be added via the [Caracal-Rails](https://github.com/trade-informatics/caracal-rails) gem.
823
+ Rails integration can be added via the [Caracal-Rails](https://github.com/urvin-compliance/caracal-rails) gem.
769
824
 
770
825
 
771
826
  ## Using Variables
772
827
 
773
- Lexical scope is a pretty big challenge for Caracal and it often confuses new users. This [closed issue](https://github.com/trade-informatics/caracal/issues/71) covers the discussion both from the user and library persepctive.
828
+ Lexical scope is a pretty big challenge for Caracal and it often confuses new users. This [closed issue](https://github.com/urvin-compliance/caracal/issues/71) covers the discussion both from the user and library persepctive.
774
829
 
775
830
 
776
831
  ## Filing an Issue
@@ -789,7 +844,7 @@ happy to answer questions and provide what guidance we can. We just won't write
789
844
 
790
845
  For those using reasonably current versions of Word, please consider the following:
791
846
 
792
- - Before you file an issue, please run the example Caracal project [caracal-example](https://github.com/trade-informatics/caracal-example) in your development environment and
847
+ - Before you file an issue, please run the example Caracal project [caracal-example](https://github.com/urvin-compliance/caracal-example) in your development environment and
793
848
  check the output. This project implements nearly every feature Caracal supports and renders the expected output
794
849
  correctly. It can be thought of as a canonical implementation of the library.
795
850
 
@@ -818,7 +873,7 @@ for 2010/2013, a set for 2016, etc.).
818
873
 
819
874
  ## Contributing
820
875
 
821
- 1. Fork it ( https://github.com/trade-informatics/caracal/fork )
876
+ 1. Fork it ( https://github.com/urvin-compliance/caracal/fork )
822
877
  2. Create your feature branch (`git checkout -b my-new-feature`)
823
878
  3. Commit your changes (`git commit -am 'Add some feature'`)
824
879
  4. Push to the branch (`git push origin my-new-feature`)
@@ -837,6 +892,6 @@ A tip of the hat to the wonderful PDF generation library [Prawn](https://github.
837
892
 
838
893
  ## License
839
894
 
840
- Copyright (c) 2014 Trade Informatics, Inc
895
+ Copyright (c) 2014 Urvin LLC
841
896
 
842
- [MIT License](https://github.com/trade-informatics/caracal/blob/master/LICENSE.txt)
897
+ [MIT License](https://github.com/urvin-compliance/caracal/blob/master/LICENSE.txt)
data/caracal.gemspec CHANGED
@@ -6,11 +6,11 @@ require 'caracal/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'caracal'
8
8
  spec.version = Caracal::VERSION
9
- spec.authors = ['Trade Infomatics', 'John Dugan']
10
- spec.email = ['jpdugan@gmail.com']
9
+ spec.authors = ['Urvin LLC', 'John Dugan', 'Dave Lauer']
10
+ spec.email = ['jpdugan@gmail.com', 'dave@plia.com']
11
11
  spec.summary = %q{ Fast, professional Microsoft Word (docx) writer for Ruby. }
12
12
  spec.description = %q{ Caracal is a pure Ruby Microsoft Word generation library that produces professional quality MSWord documents (docx) using a simple, HTML-style DSL. }
13
- spec.homepage = 'https://github.com/trade-informatics/caracal'
13
+ spec.homepage = 'https://github.com/urvin-compliance/caracal'
14
14
  spec.license = 'MIT'
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
@@ -18,11 +18,13 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
+ spec.required_ruby_version = '>= 3.1'
22
+
21
23
  spec.add_dependency 'nokogiri', '~> 1.6'
22
- spec.add_dependency 'rubyzip', '~> 1.1'
24
+ spec.add_dependency 'rubyzip', '>= 1.1.6', '< 4.0'
23
25
  spec.add_dependency 'tilt', '>= 1.4'
24
26
 
25
- spec.add_development_dependency 'bundler', '~> 1.3'
26
- spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'bundler', '>= 1.3'
28
+ spec.add_development_dependency 'rake', '>= 10.0'
27
29
  spec.add_development_dependency 'rspec', '~> 3.0'
28
30
  end
@@ -20,11 +20,16 @@ module Caracal
20
20
  model = Caracal::Core::Models::IFrameModel.new(options, &block)
21
21
  if model.valid?
22
22
  model.preprocess!
23
- model.namespaces.each do |(prefix, href)|
24
- namespace({ prefix: prefix, href: href })
25
- end
26
- model.ignorables.each do |prefix|
27
- ignorable(prefix)
23
+
24
+ # table cells can't hold namespaces; the document
25
+ # collects those when it renders.
26
+ if respond_to?(:namespace)
27
+ model.namespaces.each do |(prefix, href)|
28
+ namespace({ prefix: prefix, href: href })
29
+ end
30
+ model.ignorables.each do |prefix|
31
+ ignorable(prefix)
32
+ end
28
33
  end
29
34
 
30
35
  contents << model
@@ -18,7 +18,7 @@ module Caracal
18
18
  const_set(:DEFAULT_BORDER_COLOR, 'auto')
19
19
  const_set(:DEFAULT_BORDER_LINE, :single)
20
20
  const_set(:DEFAULT_BORDER_SIZE, 4) # 0.5pt in 1/8 points
21
- const_set(:DEFAULT_BORDER_SPACING, 1) # 0.125pt in 1/8 points
21
+ const_set(:DEFAULT_BORDER_SPACING, 1) # 1pt. w:space is measured in points, not 1/8 points.
22
22
  const_set(:DEFAULT_BORDER_TYPE, :top)
23
23
 
24
24
  # accessors
@@ -1,3 +1,4 @@
1
+ require 'tempfile'
1
2
  require 'caracal/core/models/base_model'
2
3
 
3
4
  module Caracal
@@ -88,13 +89,11 @@ module Caracal
88
89
 
89
90
  def file
90
91
  @file ||= begin
91
- if iframe_url.nil?
92
- file = Tempfile.new('iframe')
93
- file.write iframe_data
94
- file.rewind
95
- else
96
- file = open(iframe_url)
97
- end
92
+ content = iframe_url.to_s.empty? ? iframe_data : ::Caracal::Utilities.read_resource(iframe_url)
93
+ file = Tempfile.new(['iframe', '.docx'])
94
+ file.binmode
95
+ file.write content
96
+ file.close # flushes the write; rubyzip reopens by path
98
97
  file
99
98
  end
100
99
  end
@@ -27,6 +27,7 @@ module Caracal
27
27
  attr_reader :paragraph_italic
28
28
  attr_reader :paragraph_underline
29
29
  attr_reader :paragraph_bgcolor
30
+ attr_reader :paragraph_keep_next
30
31
 
31
32
  # initialization
32
33
  def initialize(options={}, &block)
@@ -63,7 +64,7 @@ module Caracal
63
64
  #========== SETTERS ===============================
64
65
 
65
66
  # booleans
66
- [:bold, :italic, :underline].each do |m|
67
+ [:bold, :italic, :keep_next, :underline].each do |m|
67
68
  define_method "#{ m }" do |value|
68
69
  instance_variable_set("@paragraph_#{ m }", !!value)
69
70
  end
@@ -24,7 +24,17 @@ module Caracal
24
24
  setting: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings',
25
25
  style: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles'
26
26
  }
27
-
27
+
28
+ IMAGE_SIGNATURES = {
29
+ "\x89PNG\r\n\x1A\n".b => 'png',
30
+ "\xFF\xD8\xFF".b => 'jpeg',
31
+ 'GIF8'.b => 'gif',
32
+ 'BM'.b => 'bmp',
33
+ "II*\x00".b => 'tiff',
34
+ "MM\x00*".b => 'tiff'
35
+ }
36
+ IMAGE_EXTENSIONS = %w(png jpeg jpg gif bmp tif tiff svg)
37
+
28
38
  # accessors
29
39
  attr_reader :relationship_id
30
40
  attr_reader :relationship_key
@@ -46,14 +56,27 @@ module Caracal
46
56
 
47
57
  def formatted_target
48
58
  if relationship_type == :image
49
- ext = relationship_target.to_s.split('.').last
50
- ext = ext.split('?').first
51
- "media/image#{ relationship_id }.#{ ext }"
59
+ "media/image#{ relationship_id }.#{ image_extension }"
52
60
  else
53
61
  relationship_target
54
62
  end
55
63
  end
56
-
64
+
65
+ # Prefers the type detected from the image bytes, then the
66
+ # extension of the target's path (ignoring any query string),
67
+ # and falls back to png so the part name is always valid.
68
+ #
69
+ def image_extension
70
+ data = relationship_data.to_s.b
71
+ if (match = IMAGE_SIGNATURES.find { |sig, _| data.start_with?(sig) })
72
+ return match.last
73
+ end
74
+
75
+ path = relationship_target.to_s.split(/[?#]/).first.to_s
76
+ ext = File.extname(path).delete('.').downcase
77
+ IMAGE_EXTENSIONS.include?(ext) ? ext : 'png'
78
+ end
79
+
57
80
  def formatted_type
58
81
  TYPE_MAP.fetch(relationship_type)
59
82
  end
@@ -21,7 +21,9 @@ module Caracal
21
21
  const_set(:DEFAULT_TABLE_BORDER_COLOR, 'auto')
22
22
  const_set(:DEFAULT_TABLE_BORDER_LINE, :single)
23
23
  const_set(:DEFAULT_TABLE_BORDER_SIZE, 0) # units in 1/8 points
24
- const_set(:DEFAULT_TABLE_BORDER_SPACING, 0)
24
+ const_set(:DEFAULT_TABLE_BORDER_SPACING, 0)
25
+ const_set(:DEFAULT_TABLE_HEADER_ROWS, 0)
26
+ const_set(:DEFAULT_TABLE_CANT_SPLIT_ROWS, {})
25
27
 
26
28
  # accessors
27
29
  attr_reader :table_align
@@ -36,7 +38,9 @@ module Caracal
36
38
  attr_reader :table_border_right # returns border model
37
39
  attr_reader :table_border_horizontal # returns border model
38
40
  attr_reader :table_border_vertical # returns border model
39
-
41
+ attr_reader :table_header_rows
42
+ attr_reader :table_column_widths
43
+
40
44
  # initialization
41
45
  def initialize(options={}, &block)
42
46
  @table_align = DEFAULT_TABLE_ALIGN
@@ -44,7 +48,9 @@ module Caracal
44
48
  @table_border_line = DEFAULT_TABLE_BORDER_LINE
45
49
  @table_border_size = DEFAULT_TABLE_BORDER_SIZE
46
50
  @table_border_spacing = DEFAULT_TABLE_BORDER_SPACING
47
-
51
+ @table_header_rows = DEFAULT_TABLE_HEADER_ROWS
52
+ @table_rows_cant_split = {}
53
+
48
54
  super options, &block
49
55
  end
50
56
 
@@ -99,8 +105,33 @@ module Caracal
99
105
  m.apply_styles(options)
100
106
  end
101
107
  end
108
+
109
+ # This method allows a user to specify one or more
110
+ # rows that should be rendered with the
111
+ # <w:cant_split /> property
112
+ #
113
+ # For example
114
+ #
115
+ # docx.table data do |t|
116
+ # t.cant_split r.rows[0...-1]
117
+ # end
118
+ #
119
+ # would cause all rows in the table to be rendered
120
+ # with the <w:cant_split /> property.
121
+ def cant_split(models, options={})
122
+ models = rows.include?(models) ? [models] : models
123
+ models.each do |r|
124
+ @table_rows_cant_split[rows.index(r)] = true
125
+ end
126
+ end
102
127
 
103
-
128
+ # This method returns true or false depending on
129
+ # if a row has been marked as a can't split row
130
+ # via the cant_split method.
131
+ def cant_split?(row_index)
132
+ !!@table_rows_cant_split[row_index]
133
+ end
134
+
104
135
  #=============== GETTERS ==============================
105
136
 
106
137
  # border attrs
@@ -121,7 +152,7 @@ module Caracal
121
152
  #=============== SETTERS ==============================
122
153
 
123
154
  # integers
124
- [:border_size, :border_spacing, :width].each do |m|
155
+ [:border_size, :border_spacing, :header_rows, :width].each do |m|
125
156
  define_method "#{ m }" do |value|
126
157
  instance_variable_set("@table_#{ m }", value.to_i)
127
158
  end
@@ -148,7 +179,12 @@ module Caracal
148
179
  instance_variable_set("@table_#{ m }", value.to_s.to_sym)
149
180
  end
150
181
  end
151
-
182
+
183
+ # column widths
184
+ def column_widths(value)
185
+ @table_column_widths = value.map(&:to_i) if value.is_a?(Array)
186
+ end
187
+
152
188
  # .data
153
189
  def data(value)
154
190
  begin
@@ -196,6 +232,7 @@ module Caracal
196
232
  k << [:data, :align, :width]
197
233
  k << [:border_color, :border_line, :border_size, :border_spacing]
198
234
  k << [:border_bottom, :border_left, :border_right, :border_top, :border_horizontal, :border_vertical]
235
+ k << [:column_widths]
199
236
  k.flatten
200
237
  end
201
238
 
@@ -203,4 +240,4 @@ module Caracal
203
240
 
204
241
  end
205
242
  end
206
- end
243
+ end
@@ -1,4 +1,3 @@
1
- require 'open-uri'
2
1
  require 'zip'
3
2
 
4
3
  require 'caracal/core/bookmarks'
@@ -136,6 +135,8 @@ module Caracal
136
135
  # a string buffer. Order is important!
137
136
  #
138
137
  def render
138
+ register_nested_iframes(contents)
139
+
139
140
  buffer = ::Zip::OutputStream.write_buffer do |zip|
140
141
  render_package_relationships(zip)
141
142
  render_content_types(zip)
@@ -168,6 +169,28 @@ module Caracal
168
169
  #------------------------------------------------------
169
170
  private
170
171
 
172
+ #============ PREPROCESSING ===========================
173
+
174
+ # Iframes inside table cells can't register their namespaces
175
+ # on the document when they're created, so collect them here.
176
+ #
177
+ def register_nested_iframes(models)
178
+ models.each do |model|
179
+ case model
180
+ when Caracal::Core::Models::IFrameModel
181
+ model.namespaces.each do |(prefix, href)|
182
+ namespace({ prefix: prefix, href: href })
183
+ end
184
+ model.ignorables.each do |prefix|
185
+ ignorable(prefix)
186
+ end
187
+ when Caracal::Core::Models::TableModel
188
+ model.cells.each { |cell| register_nested_iframes(cell.contents) }
189
+ end
190
+ end
191
+ end
192
+
193
+
171
194
  #============ RENDERERS ===============================
172
195
 
173
196
  def render_app(zip)
@@ -225,7 +248,7 @@ module Caracal
225
248
  if rel.relationship_data.to_s.size > 0
226
249
  content = rel.relationship_data
227
250
  else
228
- content = open(rel.relationship_target).read
251
+ content = ::Caracal::Utilities.read_resource(rel.relationship_target)
229
252
  end
230
253
 
231
254
  zip.put_next_entry("word/#{ rel.formatted_target }")
@@ -17,10 +17,14 @@ module Caracal
17
17
  def to_xml
18
18
  builder = ::Nokogiri::XML::Builder.with(declaration_xml) do |xml|
19
19
  xml.send 'Types', root_options do
20
+ xml.send 'Default', { 'Extension' => 'bmp', 'ContentType' => 'image/bmp' }
20
21
  xml.send 'Default', { 'Extension' => 'gif', 'ContentType' => 'image/gif' }
21
22
  xml.send 'Default', { 'Extension' => 'jpeg', 'ContentType' => 'image/jpeg' }
22
23
  xml.send 'Default', { 'Extension' => 'jpg', 'ContentType' => 'image/jpeg' }
23
24
  xml.send 'Default', { 'Extension' => 'png', 'ContentType' => 'image/png' }
25
+ xml.send 'Default', { 'Extension' => 'svg', 'ContentType' => 'image/svg+xml' }
26
+ xml.send 'Default', { 'Extension' => 'tif', 'ContentType' => 'image/tiff' }
27
+ xml.send 'Default', { 'Extension' => 'tiff', 'ContentType' => 'image/tiff' }
24
28
  xml.send 'Default', { 'Extension' => 'rels', 'ContentType' => 'application/vnd.openxmlformats-package.relationships+xml' }
25
29
  xml.send 'Default', { 'Extension' => 'xml', 'ContentType' => 'application/xml' }
26
30
  xml.send 'Override', { 'PartName' => '/docProps/app.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.extended-properties+xml' }
@@ -23,7 +23,7 @@ module Caracal
23
23
  { fmtid: '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}', pid: index + 2, name: property.custom_property_name } do
24
24
  case property.custom_property_type.downcase
25
25
  when 'text'
26
- xml['vt'].lpwstr property.custom_property_value
26
+ xml['vt'].lpwstr xml_safe(property.custom_property_value)
27
27
  when 'date'
28
28
  xml['vt'].filetime property.custom_property_value.to_date
29
29
  when 'number'
@@ -33,11 +33,11 @@ module Caracal
33
33
  xml['vt'].bool property.custom_property_value
34
34
  else
35
35
  # Not a boolean sent, so reverting to string so docx will open
36
- xml['vt'].lpwstr property.custom_property_value
36
+ xml['vt'].lpwstr xml_safe(property.custom_property_value)
37
37
  end
38
38
  else
39
39
  # Fail to string type
40
- xml['vt'].lpwstr property.custom_property_value
40
+ xml['vt'].lpwstr xml_safe(property.custom_property_value)
41
41
  end
42
42
  end
43
43
  end
@@ -214,7 +214,7 @@ module Caracal
214
214
  xml['w'].hyperlink(hyperlink_options) do
215
215
  xml['w'].r run_options do
216
216
  render_run_attributes(xml, model, false)
217
- xml['w'].t({ 'xml:space' => 'preserve' }, model.link_content)
217
+ xml['w'].t({ 'xml:space' => 'preserve' }, xml_safe(model.link_content))
218
218
  end
219
219
  end
220
220
  end
@@ -275,6 +275,7 @@ module Caracal
275
275
  xml['w'].pStyle({ 'w:val' => model.paragraph_style }) unless model.paragraph_style.nil?
276
276
  xml['w'].contextualSpacing({ 'w:val' => '0' })
277
277
  xml['w'].jc({ 'w:val' => model.paragraph_align }) unless model.paragraph_align.nil?
278
+ xml['w'].keepNext if model.paragraph_keep_next == true
278
279
  render_run_attributes(xml, model, true)
279
280
  end
280
281
  model.runs.each do |run|
@@ -299,7 +300,7 @@ module Caracal
299
300
  def render_text(xml, model)
300
301
  xml['w'].r run_options do
301
302
  render_run_attributes(xml, model, false)
302
- xml['w'].t({ 'xml:space' => 'preserve' }, model.text_content)
303
+ xml['w'].t({ 'xml:space' => 'preserve' }, xml_safe(model.text_content))
303
304
  end
304
305
  end
305
306
 
@@ -332,25 +333,28 @@ module Caracal
332
333
  xml['w'].tblLook({ 'w:val' => '0600' })
333
334
  end
334
335
  xml['w'].tblGrid do
335
- model.rows.first.each do |tc|
336
- (tc.cell_colspan || 1).times do
337
- xml['w'].gridCol({ 'w:w' => tc.cell_width })
338
- end
336
+ column_widths = model.table_column_widths
337
+ column_widths ||= model.rows.first.map do |tc|
338
+ [tc.cell_width] * (tc.cell_colspan || 1)
339
+ end.flatten
340
+
341
+ column_widths.each do |width|
342
+ xml['w'].gridCol({ 'w:w' => width })
339
343
  end
344
+
340
345
  xml['w'].tblGridChange({ 'w:id' => '0' }) do
341
346
  xml['w'].tblGrid do
342
- model.rows.first.each do |tc|
343
- (tc.cell_colspan || 1).times do
344
- xml['w'].gridCol({ 'w:w' => tc.cell_width })
345
- end
347
+ column_widths.each do |width|
348
+ xml['w'].gridCol({ 'w:w' => width })
346
349
  end
347
350
  end
348
351
  end
349
352
  end
350
353
 
351
354
  rowspan_hash = {}
352
- model.rows.each do |row|
355
+ model.rows.each.with_index do |row, row_index|
353
356
  xml['w'].tr do
357
+ render_table_row_properties(xml, model, row_index)
354
358
  tc_index = 0
355
359
  row.each do |tc|
356
360
  xml['w'].tc do
@@ -390,6 +394,13 @@ module Caracal
390
394
  end
391
395
  end
392
396
 
397
+ def render_table_row_properties(xml, model, index)
398
+ xml['w'].trPr do
399
+ xml['w'].cantSplit if model.cant_split?(index)
400
+ xml['w'].tblHeader if index < model.table_header_rows
401
+ end
402
+ end
403
+
393
404
 
394
405
  #============= OPTIONS ===================================
395
406
 
@@ -31,7 +31,7 @@ module Caracal
31
31
  end
32
32
  end
33
33
  xml['w'].t({ 'xml:space' => 'preserve' }) do
34
- xml.text "#{ document.page_number_label } "
34
+ xml.text "#{ xml_safe(document.page_number_label) } "
35
35
  end
36
36
  end
37
37
  end
@@ -71,6 +71,14 @@ module Caracal
71
71
  { 'w:rsidR' => '00000000', 'w:rsidRPr' => '00000000', 'w:rsidDel' => '00000000' }
72
72
  end
73
73
 
74
+ # This method removes control characters that XML 1.0 doesn't
75
+ # allow. A single one makes Word reject the whole document.
76
+ #
77
+ def xml_safe(value)
78
+ str = value.to_s
79
+ str.valid_encoding? ? str.gsub(/[\x00-\x08\x0B\x0C\x0E-\x1F]/, '') : str
80
+ end
81
+
74
82
  # These save options force Nokogiri to remove indentation and
75
83
  # line feeds from the output.
76
84
  #
@@ -1,3 +1,5 @@
1
+ require 'open-uri'
2
+
1
3
  # We're using this strategy borrowed from ActiveSupport to
2
4
  # make command syntax a little more flexible. In a perfect
3
5
  # world we'd just use the double splat feature of Ruby, but
@@ -18,6 +20,19 @@ module Caracal
18
20
  {}
19
21
  end
20
22
  end
21
-
23
+
24
+ # Reads binary content from an http(s) URL or a local file path.
25
+ # Avoids Kernel#open, which no longer fetches URLs on Ruby 3+,
26
+ # reads in text mode on Windows, and runs a leading "|" as a
27
+ # shell command.
28
+ def self.read_resource(location)
29
+ location = location.to_s
30
+ if location =~ /\Ahttps?:\/\//i
31
+ URI.open(location, 'rb', &:read)
32
+ else
33
+ File.binread(location)
34
+ end
35
+ end
36
+
22
37
  end
23
38
  end
@@ -1,3 +1,3 @@
1
1
  module Caracal
2
- VERSION = '1.4.1'
2
+ VERSION = '1.5.0'
3
3
  end
data/lib/caracal.rb CHANGED
@@ -32,6 +32,7 @@ end
32
32
  # and Caracal::Core::Models::TableCellModel.
33
33
  #
34
34
  Caracal::Core::Models::TableCellModel.class_eval do
35
+ include Caracal::Core::IFrames
35
36
  include Caracal::Core::Images
36
37
  include Caracal::Core::Lists
37
38
  include Caracal::Core::Rules
@@ -11,6 +11,7 @@ describe Caracal::Core::Models::ParagraphModel do
11
11
  italic false
12
12
  underline true
13
13
  bgcolor 'cccccc'
14
+ keep_next true
14
15
  end
15
16
  end
16
17
 
@@ -31,6 +32,7 @@ describe Caracal::Core::Models::ParagraphModel do
31
32
  it { expect(subject.paragraph_italic).to eq false }
32
33
  it { expect(subject.paragraph_underline).to eq true }
33
34
  it { expect(subject.paragraph_bgcolor).to eq 'cccccc' }
35
+ it { expect(subject.paragraph_keep_next).to eq true }
34
36
  end
35
37
 
36
38
  end
@@ -70,6 +72,11 @@ describe Caracal::Core::Models::ParagraphModel do
70
72
 
71
73
  it { expect(subject.paragraph_italic).to eq true }
72
74
  end
75
+ describe '.keep_next' do
76
+ before { subject.keep_next(true) }
77
+
78
+ it { expect(subject.paragraph_keep_next).to eq true }
79
+ end
73
80
  describe '.underline' do
74
81
  before { subject.underline(true) }
75
82
 
@@ -72,6 +72,24 @@ describe Caracal::Core::Models::TableModel do
72
72
  describe '.cells' do
73
73
  it { expect(subject.cells[0]).to eq 'top left' }
74
74
  end
75
+
76
+ # .cant_split and cant_split?
77
+ describe '.cant_split and .cant_split?' do
78
+ it { expect(subject.cant_split?(0)).to eq(false) }
79
+
80
+ describe "when first row can't split" do
81
+ before { subject.cant_split subject.rows[0] }
82
+
83
+ it { expect(subject.cant_split?(0)).to eq(true) }
84
+ end
85
+
86
+ describe 'when no rows can be split' do
87
+ before { subject.cant_split subject.rows }
88
+
89
+ it { expect(subject.cant_split?(0)).to eq(true) }
90
+ it { expect(subject.cant_split?(1)).to eq(true) }
91
+ end
92
+ end
75
93
  end
76
94
 
77
95
 
@@ -178,6 +196,13 @@ describe Caracal::Core::Models::TableModel do
178
196
 
179
197
  it { expect(subject.table_width).to eq 7500 }
180
198
  end
199
+
200
+ # .column_widths
201
+ describe '.column_widths' do
202
+ before { subject.column_widths([2400, '1200']) }
203
+
204
+ it { expect(subject.table_column_widths).to eq [2400, 1200] }
205
+ end
181
206
 
182
207
 
183
208
 
@@ -209,7 +234,7 @@ describe Caracal::Core::Models::TableModel do
209
234
  # .option_keys
210
235
  describe '.option_keys' do
211
236
  let(:actual) { subject.send(:option_keys).sort }
212
- let(:expected1) { [:data, :align, :width] }
237
+ let(:expected1) { [:data, :align, :width, :column_widths] }
213
238
  let(:expected2) { [:border_color, :border_line, :border_size, :border_spacing] }
214
239
  let(:expected3) { [:border_top, :border_bottom, :border_left, :border_right, :border_horizontal, :border_vertical] }
215
240
  let(:expected) { (expected1 + expected2 + expected3).sort }
@@ -219,4 +244,4 @@ describe Caracal::Core::Models::TableModel do
219
244
 
220
245
  end
221
246
 
222
- end
247
+ end
@@ -18,8 +18,15 @@ describe Caracal::Core::Tables do
18
18
 
19
19
  it { expect(subject.contents.size).to eq size + 1 }
20
20
  it { expect(subject.contents.last).to be_a(Caracal::Core::Models::TableModel) }
21
+ it { expect(subject.contents.last.table_header_rows).to eq(0) }
22
+
23
+ context 'when .header_rows' do
24
+ before { subject.table [['Sample Text']] do header_rows 1 end }
25
+
26
+ it { expect(subject.contents.last.table_header_rows).to eq(1) }
27
+ end
21
28
  end
22
29
 
23
30
  end
24
31
 
25
- end
32
+ end
@@ -0,0 +1,212 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+ require 'tempfile'
4
+
5
+ describe Caracal::Document do
6
+ W_NS = { 'w' => 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' }
7
+
8
+ let(:png) { 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='.unpack1('m') }
9
+ let(:jpeg) { "\xFF\xD8\xFF\xE0".b + 'rest of jpeg' }
10
+ # the PNG plus bytes a text-mode read would mangle on Windows
11
+ let(:image_bytes) { png + "\r\n\x1A\r\n".b }
12
+
13
+ def parts(docx)
14
+ result = {}
15
+ Zip::File.open_buffer(StringIO.new(docx.render.string)) do |zip|
16
+ zip.each { |e| result[e.name] = e.get_input_stream.read }
17
+ end
18
+ result
19
+ end
20
+
21
+ def strict_xml(str)
22
+ Nokogiri::XML(str) { |config| config.strict }
23
+ end
24
+
25
+
26
+ #-------------------------------------------------------------
27
+ # Control characters
28
+ #-------------------------------------------------------------
29
+
30
+ describe 'text containing characters XML does not allow' do
31
+ let(:text) { "a\x01b\x0Bc\td" }
32
+
33
+ it 'strips them from paragraphs, links and table cells' do
34
+ text = self.text
35
+ docx = described_class.new('test.docx')
36
+ docx.p text
37
+ docx.p { link text, 'https://www.example.com' }
38
+ docx.table [[text]]
39
+
40
+ xml = parts(docx)['word/document.xml']
41
+ expect { strict_xml(xml) }.not_to raise_error
42
+ expect(xml.scan("abc\td").size).to eq 3
43
+ end
44
+
45
+ it 'strips them from custom properties' do
46
+ text = self.text
47
+ docx = described_class.new('test.docx')
48
+ docx.custom_property { |p| p.name 'note'; p.value text; p.type :text }
49
+
50
+ expect { strict_xml(parts(docx)['docProps/custom.xml']) }.not_to raise_error
51
+ end
52
+ end
53
+
54
+
55
+ #-------------------------------------------------------------
56
+ # Image part names
57
+ #-------------------------------------------------------------
58
+
59
+ describe 'image part names' do
60
+ it 'uses the type of the supplied data rather than the URL' do
61
+ png, jpeg = self.png, self.jpeg
62
+ docx = described_class.new('test.docx')
63
+ docx.img('https://bucket.example.com/rails/blobs/abc123?signature=x', width: 10, height: 10) { |i| i.data png }
64
+ docx.img('https://bucket.example.com/photo.png?signature=y', width: 10, height: 10) { |i| i.data jpeg }
65
+
66
+ media = parts(docx).keys.grep(%r{\Aword/media/})
67
+ expect(media.map { |name| File.extname(name) }).to eq %w(.png .jpeg)
68
+ end
69
+
70
+ {
71
+ 'https://example.com/a/photo.JPG?x=1#frag' => 'jpg',
72
+ 'https://example.com/rails/blobs/abc' => 'png',
73
+ 'https://example.com' => 'png',
74
+ '/tmp/picture.gif' => 'gif',
75
+ 'Picture 1' => 'png'
76
+ }.each do |target, ext|
77
+ it "names a #{ target.inspect } target with .#{ ext } when no data is supplied" do
78
+ rel = Caracal::Core::Models::RelationshipModel.new(id: 1, type: :image, target: target)
79
+ expect(rel.formatted_target).to eq "media/image1.#{ ext }"
80
+ end
81
+ end
82
+
83
+ it 'registers content types for bmp, tiff and svg' do
84
+ xml = parts(described_class.new('test.docx'))['[Content_Types].xml']
85
+ %w(bmp tif tiff svg).each { |ext| expect(xml).to include %(Extension="#{ ext }") }
86
+ end
87
+ end
88
+
89
+
90
+ #-------------------------------------------------------------
91
+ # Image sources
92
+ #-------------------------------------------------------------
93
+
94
+ describe 'image sources' do
95
+ def media(docx)
96
+ parts(docx).select { |name, _| name.start_with?('word/media/') }.values
97
+ end
98
+
99
+ it 'embeds a local image byte-for-byte' do
100
+ file = Tempfile.new(['image', '.png'])
101
+ file.binmode
102
+ file.write(image_bytes)
103
+ file.close
104
+
105
+ docx = described_class.new('test.docx')
106
+ docx.img file.path, width: 10, height: 10
107
+
108
+ expect(media(docx)).to eq [image_bytes]
109
+ end
110
+
111
+ it 'fetches an http(s) image via URI.open' do
112
+ url = 'https://www.example.com/image.png'
113
+ bytes = image_bytes
114
+ expect(URI).to receive(:open).with(url, 'rb') { |*_, &blk| blk.call(StringIO.new(bytes)) }
115
+
116
+ docx = described_class.new('test.docx')
117
+ docx.img url, width: 10, height: 10
118
+
119
+ expect(media(docx)).to eq [bytes]
120
+ end
121
+
122
+ it 'does not run a "|" target as a shell command' do
123
+ marker = File.join(Dir.tmpdir, "caracal-pipe-#{ Process.pid }")
124
+ docx = described_class.new('test.docx')
125
+ docx.img "|touch #{ marker }", width: 10, height: 10
126
+
127
+ expect { docx.render }.to raise_error(Errno::ENOENT)
128
+ expect(File.exist?(marker)).to eq false
129
+ end
130
+ end
131
+
132
+
133
+ #-------------------------------------------------------------
134
+ # Table column widths
135
+ #-------------------------------------------------------------
136
+
137
+ describe 'table grid column widths' do
138
+ let(:data) do
139
+ [
140
+ [{ content: 'First', width: 2400 }, { content: 'Second', width: 1200 }],
141
+ [{ content: 'Third', width: 1800 }, { content: 'Fourth', width: 1800 }]
142
+ ]
143
+ end
144
+
145
+ it 'uses explicit column widths when set' do
146
+ docx = described_class.new('test.docx')
147
+ docx.table(data) { column_widths [1000, 2600] }
148
+
149
+ xml = strict_xml(parts(docx)['word/document.xml'])
150
+ expect(xml.xpath('//w:tbl/w:tblGrid/w:gridCol/@w:w', W_NS).map(&:value)).to eq %w(1000 2600)
151
+ end
152
+
153
+ it 'falls back to the first row widths otherwise' do
154
+ docx = described_class.new('test.docx')
155
+ docx.table(data)
156
+
157
+ xml = strict_xml(parts(docx)['word/document.xml'])
158
+ expect(xml.xpath('//w:tbl/w:tblGrid/w:gridCol/@w:w', W_NS).map(&:value)).to eq %w(2400 1200)
159
+ end
160
+ end
161
+
162
+
163
+ #-------------------------------------------------------------
164
+ # Iframes
165
+ #-------------------------------------------------------------
166
+
167
+ describe 'iframes' do
168
+ let(:snippet) do
169
+ inner = described_class.new('inner.docx')
170
+ inner.p 'Text from the iframe'
171
+ inner.render.string
172
+ end
173
+
174
+ it 'renders an iframe at the top level' do
175
+ docx = described_class.new('test.docx')
176
+ docx.iframe data: snippet
177
+
178
+ xml = strict_xml(parts(docx)['word/document.xml'])
179
+ expect(xml.at_xpath('//w:body/w:p/w:r/w:t[.="Text from the iframe"]', W_NS)).not_to be_nil
180
+ end
181
+
182
+ it 'renders an iframe inside a table cell' do
183
+ data = snippet
184
+ cell = Caracal::Core::Models::TableCellModel.new { |c| c.iframe(data: data) }
185
+ docx = described_class.new('test.docx')
186
+ docx.table [[cell]]
187
+
188
+ xml = strict_xml(parts(docx)['word/document.xml'])
189
+ expect(xml.at_xpath('//w:tc//w:t[.="Text from the iframe"]', W_NS)).not_to be_nil
190
+ end
191
+
192
+ it 'uses the data when the url is explicitly nil' do
193
+ docx = described_class.new('test.docx')
194
+ docx.iframe url: nil, data: snippet
195
+
196
+ xml = strict_xml(parts(docx)['word/document.xml'])
197
+ expect(xml.at_xpath('//w:body/w:p/w:r/w:t[.="Text from the iframe"]', W_NS)).not_to be_nil
198
+ end
199
+
200
+ it 'renders an iframe fetched from a URL' do
201
+ url = 'https://www.example.com/snippet.docx'
202
+ data = snippet
203
+ expect(URI).to receive(:open).with(url, 'rb') { |*_, &blk| blk.call(StringIO.new(data)) }
204
+
205
+ docx = described_class.new('test.docx')
206
+ docx.iframe url: url
207
+
208
+ xml = strict_xml(parts(docx)['word/document.xml'])
209
+ expect(xml.at_xpath('//w:body/w:p/w:r/w:t[.="Text from the iframe"]', W_NS)).not_to be_nil
210
+ end
211
+ end
212
+ end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caracal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
- - Trade Infomatics
7
+ - Urvin LLC
8
8
  - John Dugan
9
- autorequire:
9
+ - Dave Lauer
10
+ autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2019-05-14 00:00:00.000000000 Z
13
+ date: 2026-09-16 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: nokogiri
@@ -29,16 +30,22 @@ dependencies:
29
30
  name: rubyzip
30
31
  requirement: !ruby/object:Gem::Requirement
31
32
  requirements:
32
- - - "~>"
33
+ - - ">="
33
34
  - !ruby/object:Gem::Version
34
- version: '1.1'
35
+ version: 1.1.6
36
+ - - "<"
37
+ - !ruby/object:Gem::Version
38
+ version: '4.0'
35
39
  type: :runtime
36
40
  prerelease: false
37
41
  version_requirements: !ruby/object:Gem::Requirement
38
42
  requirements:
39
- - - "~>"
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: 1.1.6
46
+ - - "<"
40
47
  - !ruby/object:Gem::Version
41
- version: '1.1'
48
+ version: '4.0'
42
49
  - !ruby/object:Gem::Dependency
43
50
  name: tilt
44
51
  requirement: !ruby/object:Gem::Requirement
@@ -57,28 +64,28 @@ dependencies:
57
64
  name: bundler
58
65
  requirement: !ruby/object:Gem::Requirement
59
66
  requirements:
60
- - - "~>"
67
+ - - ">="
61
68
  - !ruby/object:Gem::Version
62
69
  version: '1.3'
63
70
  type: :development
64
71
  prerelease: false
65
72
  version_requirements: !ruby/object:Gem::Requirement
66
73
  requirements:
67
- - - "~>"
74
+ - - ">="
68
75
  - !ruby/object:Gem::Version
69
76
  version: '1.3'
70
77
  - !ruby/object:Gem::Dependency
71
78
  name: rake
72
79
  requirement: !ruby/object:Gem::Requirement
73
80
  requirements:
74
- - - "~>"
81
+ - - ">="
75
82
  - !ruby/object:Gem::Version
76
83
  version: '10.0'
77
84
  type: :development
78
85
  prerelease: false
79
86
  version_requirements: !ruby/object:Gem::Requirement
80
87
  requirements:
81
- - - "~>"
88
+ - - ">="
82
89
  - !ruby/object:Gem::Version
83
90
  version: '10.0'
84
91
  - !ruby/object:Gem::Dependency
@@ -99,12 +106,13 @@ description: " Caracal is a pure Ruby Microsoft Word generation library that pro
99
106
  professional quality MSWord documents (docx) using a simple, HTML-style DSL. "
100
107
  email:
101
108
  - jpdugan@gmail.com
109
+ - dave@plia.com
102
110
  executables: []
103
111
  extensions: []
104
112
  extra_rdoc_files: []
105
113
  files:
114
+ - ".github/workflows/ci.yml"
106
115
  - ".gitignore"
107
- - ".travis.yml"
108
116
  - CHANGELOG.md
109
117
  - Gemfile
110
118
  - LICENSE.txt
@@ -212,14 +220,15 @@ files:
212
220
  - spec/lib/caracal/core/styles_spec.rb
213
221
  - spec/lib/caracal/core/tables_spec.rb
214
222
  - spec/lib/caracal/core/text_spec.rb
223
+ - spec/lib/caracal/document_output_spec.rb
215
224
  - spec/lib/caracal/errors_spec.rb
216
225
  - spec/spec_helper.rb
217
226
  - spec/support/_fixtures/snippet.docx
218
- homepage: https://github.com/trade-informatics/caracal
227
+ homepage: https://github.com/urvin-compliance/caracal
219
228
  licenses:
220
229
  - MIT
221
230
  metadata: {}
222
- post_install_message:
231
+ post_install_message:
223
232
  rdoc_options: []
224
233
  require_paths:
225
234
  - lib
@@ -227,16 +236,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
227
236
  requirements:
228
237
  - - ">="
229
238
  - !ruby/object:Gem::Version
230
- version: '0'
239
+ version: '3.1'
231
240
  required_rubygems_version: !ruby/object:Gem::Requirement
232
241
  requirements:
233
242
  - - ">="
234
243
  - !ruby/object:Gem::Version
235
244
  version: '0'
236
245
  requirements: []
237
- rubyforge_project:
238
- rubygems_version: 2.7.6
239
- signing_key:
246
+ rubygems_version: 3.5.22
247
+ signing_key:
240
248
  specification_version: 4
241
249
  summary: Fast, professional Microsoft Word (docx) writer for Ruby.
242
250
  test_files:
@@ -280,6 +288,7 @@ test_files:
280
288
  - spec/lib/caracal/core/styles_spec.rb
281
289
  - spec/lib/caracal/core/tables_spec.rb
282
290
  - spec/lib/caracal/core/text_spec.rb
291
+ - spec/lib/caracal/document_output_spec.rb
283
292
  - spec/lib/caracal/errors_spec.rb
284
293
  - spec/spec_helper.rb
285
294
  - spec/support/_fixtures/snippet.docx
data/.travis.yml DELETED
@@ -1,20 +0,0 @@
1
- language: ruby
2
- cache: bundler
3
- rvm:
4
- - 1.9.2
5
- - 1.9.3
6
- - 2.0.0
7
- - 2.1
8
- - rbx-2
9
- - ruby-head
10
- - jruby-head
11
- matrix:
12
- include:
13
- - rvm: jruby
14
- env: JRUBY_OPTS="--1.9 --debug"
15
- - rvm: jruby
16
- env: JRUBY_OPTS="--2.0 --debug"
17
- allow_failures:
18
- - rvm: ruby-head
19
- - rvm: jruby-head
20
- fast_finish: true