stationery 0.8.0 → 0.10.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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +251 -37
  4. data/lib/stationery/canvas.rb +3 -1
  5. data/lib/stationery/{svg → css}/selector.rb +1 -1
  6. data/lib/stationery/{svg → css}/stylesheet.rb +20 -6
  7. data/lib/stationery/css.rb +20 -0
  8. data/lib/stationery/document.rb +35 -7
  9. data/lib/stationery/elements/forms.rb +11 -6
  10. data/lib/stationery/elements/rich.rb +21 -6
  11. data/lib/stationery/elements.rb +20 -7
  12. data/lib/stationery/errors.rb +4 -0
  13. data/lib/stationery/fonts/fallback.rb +1 -1
  14. data/lib/stationery/fonts/font.rb +28 -18
  15. data/lib/stationery/forms/acro_form.rb +95 -25
  16. data/lib/stationery/forms/appearance.rb +92 -30
  17. data/lib/stationery/forms/field.rb +20 -11
  18. data/lib/stationery/forms/metrics.rb +3 -17
  19. data/lib/stationery/forms/typeface.rb +113 -0
  20. data/lib/stationery/html/css.rb +276 -0
  21. data/lib/stationery/html/document.rb +10 -2
  22. data/lib/stationery/html/tokenizer.rb +15 -4
  23. data/lib/stationery/html/tree_builder/blocks.rb +90 -40
  24. data/lib/stationery/html/tree_builder/collector.rb +9 -3
  25. data/lib/stationery/html/tree_builder.rb +44 -4
  26. data/lib/stationery/layout/flow.rb +29 -0
  27. data/lib/stationery/layout/image.rb +21 -7
  28. data/lib/stationery/layout/mark.rb +2 -0
  29. data/lib/stationery/layout/node.rb +4 -0
  30. data/lib/stationery/layout/table/cell.rb +14 -4
  31. data/lib/stationery/layout/table.rb +72 -14
  32. data/lib/stationery/layout/text.rb +47 -10
  33. data/lib/stationery/markdown/block_parser/containers.rb +21 -0
  34. data/lib/stationery/markdown/block_parser.rb +9 -3
  35. data/lib/stationery/markdown/document.rb +8 -2
  36. data/lib/stationery/markdown/inline_parser/emphasis.rb +17 -7
  37. data/lib/stationery/markdown/inline_parser/links.rb +17 -12
  38. data/lib/stationery/markdown/inline_parser/nodes.rb +24 -10
  39. data/lib/stationery/markdown/inline_parser.rb +9 -3
  40. data/lib/stationery/minitest.rb +4 -0
  41. data/lib/stationery/pdf/assembler.rb +31 -11
  42. data/lib/stationery/pdf/conformance.rb +7 -5
  43. data/lib/stationery/pdf/serializer.rb +3 -1
  44. data/lib/stationery/pdf/signature/cms.rb +95 -0
  45. data/lib/stationery/pdf/signature/timestamp.rb +120 -0
  46. data/lib/stationery/pdf/signature.rb +174 -0
  47. data/lib/stationery/pdf/types.rb +4 -0
  48. data/lib/stationery/pdf/writer.rb +73 -19
  49. data/lib/stationery/rich/nesting.rb +30 -0
  50. data/lib/stationery/rich/nodes.rb +37 -11
  51. data/lib/stationery/rich/renderer/boxes.rb +109 -0
  52. data/lib/stationery/rich/renderer/indents.rb +43 -0
  53. data/lib/stationery/rich/renderer/inlines.rb +9 -2
  54. data/lib/stationery/rich/renderer.rb +49 -16
  55. data/lib/stationery/svg/document.rb +8 -4
  56. data/lib/stationery/svg/parser.rb +51 -11
  57. data/lib/stationery/svg/style.rb +4 -6
  58. data/lib/stationery/svg/walker.rb +2 -0
  59. data/lib/stationery/tagging/element.rb +1 -1
  60. data/lib/stationery/testing/inspector.rb +98 -0
  61. data/lib/stationery/testing/matchers.rb +25 -0
  62. data/lib/stationery/text/breaks.rb +123 -0
  63. data/lib/stationery/text/paragraph.rb +8 -0
  64. data/lib/stationery/text/runs_builder.rb +4 -0
  65. data/lib/stationery/text/wrapper.rb +69 -11
  66. data/lib/stationery/version.rb +1 -1
  67. data/lib/stationery/warnings.rb +13 -0
  68. data/lib/stationery.rb +6 -2
  69. metadata +13 -3
@@ -3,7 +3,8 @@
3
3
  module Stationery
4
4
  module Text
5
5
  # Greedy line breaking over styled runs. Breaks at spaces (the space is
6
- # dropped at the break) and after hyphens; a word that does not fit is
6
+ # dropped at the break), after hyphens, at a zero-width space (U+200B,
7
+ # HTML's <wbr>) and between ideographic characters (see Breaks); a word that does not fit is
7
8
  # hyphenated when it may be (a soft hyphen U+00AD names the break points,
8
9
  # else the style's `hyphenate` language), and a word wider than the line
9
10
  # is broken between characters. A word that changes style midway
@@ -12,6 +13,7 @@ module Stationery
12
13
  class Wrapper
13
14
  TOKEN = /\n|[ \t]+|[^ \t\n-]*-+|[^ \t\n-]+/
14
15
  SOFT_HYPHEN = "­"
16
+ ZERO_WIDTH_SPACE = Breaks::ZERO_WIDTH_SPACE
15
17
  EPSILON = 0.0001
16
18
 
17
19
  Segment = Data.define(:text, :style)
@@ -26,6 +28,7 @@ module Stationery
26
28
  @current = []
27
29
  @pending_space = []
28
30
  @fallback = fallback_style
31
+ @widths = {}.compare_by_identity
29
32
  items(runs).each { |item| place(item) }
30
33
  finish unless @current.empty? && @lines.any? && !@ended_with_newline
31
34
  @lines
@@ -33,22 +36,67 @@ module Stationery
33
36
 
34
37
  private
35
38
 
36
- # [:word, segments] | [:space, segments] | [:newline]
39
+ # [:word, segments] | [:space, segments] | [:newline]. A word that runs
40
+ # on into the next styled run stays one word ("<b>Tot</b>al"); a
41
+ # zero-width space is an empty :space, a break that draws nothing; a
42
+ # word with CJK characters is cut into its break units (Breaks).
37
43
  def items(runs)
38
44
  items = []
45
+ @join = false # whether the next token may continue the last word
39
46
  runs.each do |run|
40
- run.text.scan(TOKEN) do |token|
41
- if token == "\n" then items << [:newline]
42
- elsif token.match?(/\A[ \t]/) then items << [:space, [Segment.new(token, run.style)]]
43
- elsif items.last&.first == :word && !items.last.last.last.text.end_with?("-")
44
- items.last.last << Segment.new(token, run.style)
45
- else items << [:word, [Segment.new(token, run.style)]]
47
+ if run.text.include?(ZERO_WIDTH_SPACE)
48
+ run.text.split(ZERO_WIDTH_SPACE, -1).each_with_index do |part, index|
49
+ if index.positive?
50
+ items << [:space, []]
51
+ @join = false
52
+ end
53
+ tokens(items, part, run.style)
46
54
  end
55
+ else
56
+ tokens(items, run.text, run.style)
47
57
  end
48
58
  end
49
59
  items
50
60
  end
51
61
 
62
+ def tokens(items, text, style)
63
+ text.scan(TOKEN) do |token|
64
+ if token == "\n"
65
+ items << [:newline]
66
+ @join = false
67
+ elsif token.match?(/\A[ \t]/)
68
+ items << [:space, [Segment.new(token, style)]]
69
+ @join = false
70
+ else
71
+ word_units(items, token, style)
72
+ end
73
+ end
74
+ end
75
+
76
+ # A Latin word is one unit and may run on into the next styled run
77
+ # unless it ends in a hyphen; a word with CJK characters is cut into
78
+ # its break units, none of which the next token continues. Latin words
79
+ # skip the unit split, so they cost no allocation of their own.
80
+ def word_units(items, token, style)
81
+ if Breaks.cjk?(token)
82
+ Breaks.units(token).each do |text, glued|
83
+ add_unit(items, text, style, glued)
84
+ @join = false
85
+ end
86
+ else
87
+ add_unit(items, token, style, false)
88
+ @join = !token.end_with?("-")
89
+ end
90
+ end
91
+
92
+ def add_unit(items, text, style, glued)
93
+ if (@join || glued) && items.last&.first == :word
94
+ items.last.last << Segment.new(text, style)
95
+ else
96
+ items << [:word, [Segment.new(text, style)]]
97
+ end
98
+ end
99
+
52
100
  def place((kind, segments))
53
101
  @ended_with_newline = kind == :newline
54
102
  case kind
@@ -97,6 +145,8 @@ module Stationery
97
145
  # its hyphen) fits `available`; nil when the word offers no break that
98
146
  # fits. Soft hyphens name the breaks and suppress the patterns.
99
147
  def hyphenated(word, available)
148
+ return unless @explicit || word.first&.style&.hyphenate
149
+
100
150
  chars = word.flat_map { |segment| segment.text.chars.map { |char| Segment.new(char, segment.style) } }
101
151
  break_points(chars).reverse_each do |index|
102
152
  head = chars.first(index).reject { |segment| segment.text == SOFT_HYPHEN }
@@ -112,6 +162,8 @@ module Stationery
112
162
  return (1...chars.length).select { |index| chars[index - 1].text == SOFT_HYPHEN } if @explicit
113
163
 
114
164
  text = chars.map(&:text).join
165
+ return [] if Breaks.cjk?(text)
166
+
115
167
  language = chars.first.style.hyphenate or return []
116
168
  core = text[/\p{L}+/] or return []
117
169
  offset = text.index(core)
@@ -164,7 +216,7 @@ module Stationery
164
216
  def fragments(segments)
165
217
  x = 0
166
218
  segments.chunk_while { |a, b| a.style == b.style }.filter_map do |group|
167
- text = group.map(&:text).join.delete(SOFT_HYPHEN)
219
+ text = plain(group.map(&:text).join)
168
220
  next if text.empty?
169
221
 
170
222
  font, face = @book.resolve(group.first.style)
@@ -178,12 +230,18 @@ module Stationery
178
230
  end
179
231
 
180
232
  def line_width = width(@current)
181
- def characters(segments) = segments.sum { |segment| segment.text.delete(SOFT_HYPHEN).length }
233
+ def characters(segments) = segments.sum { |segment| plain(segment.text).length }
182
234
 
235
+ # A line is summed again for every word that joins it; its segments
236
+ # are the same objects each time, so each is measured once per wrap.
183
237
  def width(segments)
184
- segments.sum { |segment| measure(segment.text.delete(SOFT_HYPHEN), segment.style) }
238
+ segments.sum { |segment| @widths[segment] ||= measure(plain(segment.text), segment.style) }
185
239
  end
186
240
 
241
+ # The text without its soft hyphens; the same String when it has none,
242
+ # which is nearly always, so measuring a word allocates nothing.
243
+ def plain(text) = text.include?(SOFT_HYPHEN) ? text.delete(SOFT_HYPHEN) : text
244
+
187
245
  def measure(text, style)
188
246
  font = @book.resolve(style).first
189
247
  font.width_of(text, style.render_size, letter_spacing: style.letter_spacing, kerning: style.kerning,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stationery
4
- VERSION = "0.8.0"
4
+ VERSION = "0.10.0"
5
5
  end
@@ -43,6 +43,19 @@ module Stationery
43
43
  def message = %(link "#{href}" dropped: scheme not allowed)
44
44
  end
45
45
 
46
+ NestingLimit = Data.define(:depth, :limit) do
47
+ def message = "content nested #{depth} levels deep was flattened below level #{limit}"
48
+ end
49
+
50
+ UnsupportedCss = Data.define(:properties, :selectors) do
51
+ def message
52
+ parts = []
53
+ parts << "properties #{properties.join(", ")}" if properties.any?
54
+ parts << "selectors #{selectors.join(", ")}" if selectors.any?
55
+ "html styles not read: #{parts.join("; ")}"
56
+ end
57
+ end
58
+
46
59
  DuplicateAnchor = Data.define(:name, :page) do
47
60
  def message = %(anchor "#{name}" on page #{page} is already defined)
48
61
  end
data/lib/stationery.rb CHANGED
@@ -66,7 +66,11 @@ require_relative "stationery/pdf/attachments"
66
66
  require_relative "stationery/pdf/xmp"
67
67
  require_relative "stationery/pdf/conformance"
68
68
  require_relative "stationery/pdf/factur_x"
69
+ require_relative "stationery/pdf/signature"
70
+ require_relative "stationery/pdf/signature/cms"
71
+ require_relative "stationery/pdf/signature/timestamp"
69
72
  require_relative "stationery/forms/metrics"
73
+ require_relative "stationery/forms/typeface"
70
74
  require_relative "stationery/forms/field"
71
75
  require_relative "stationery/forms/appearance"
72
76
  require_relative "stationery/forms/acro_form"
@@ -79,6 +83,7 @@ require_relative "stationery/text/entities"
79
83
  require_relative "stationery/text/markup"
80
84
  require_relative "stationery/text/runs_builder"
81
85
  require_relative "stationery/text/line"
86
+ require_relative "stationery/text/breaks"
82
87
  require_relative "stationery/text/wrapper"
83
88
  require_relative "stationery/text/paragraph"
84
89
  require_relative "stationery/layout/node"
@@ -103,8 +108,7 @@ require_relative "stationery/svg/path_data"
103
108
  require_relative "stationery/svg/arc"
104
109
  require_relative "stationery/svg/parser"
105
110
  require_relative "stationery/svg/transform"
106
- require_relative "stationery/svg/selector"
107
- require_relative "stationery/svg/stylesheet"
111
+ require_relative "stationery/css"
108
112
  require_relative "stationery/svg/style"
109
113
  require_relative "stationery/svg/shapes"
110
114
  require_relative "stationery/svg/bounds"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stationery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -40,6 +40,9 @@ files:
40
40
  - lib/stationery/cli/render.rb
41
41
  - lib/stationery/color.rb
42
42
  - lib/stationery/component.rb
43
+ - lib/stationery/css.rb
44
+ - lib/stationery/css/selector.rb
45
+ - lib/stationery/css/stylesheet.rb
43
46
  - lib/stationery/document.rb
44
47
  - lib/stationery/elements.rb
45
48
  - lib/stationery/elements/forms.rb
@@ -88,7 +91,9 @@ files:
88
91
  - lib/stationery/forms/appearance.rb
89
92
  - lib/stationery/forms/field.rb
90
93
  - lib/stationery/forms/metrics.rb
94
+ - lib/stationery/forms/typeface.rb
91
95
  - lib/stationery/geometry.rb
96
+ - lib/stationery/html/css.rb
92
97
  - lib/stationery/html/document.rb
93
98
  - lib/stationery/html/tokenizer.rb
94
99
  - lib/stationery/html/tree_builder.rb
@@ -158,6 +163,9 @@ files:
158
163
  - lib/stationery/pdf/outline_writer.rb
159
164
  - lib/stationery/pdf/page_labels.rb
160
165
  - lib/stationery/pdf/serializer.rb
166
+ - lib/stationery/pdf/signature.rb
167
+ - lib/stationery/pdf/signature/cms.rb
168
+ - lib/stationery/pdf/signature/timestamp.rb
161
169
  - lib/stationery/pdf/stream.rb
162
170
  - lib/stationery/pdf/types.rb
163
171
  - lib/stationery/pdf/writer.rb
@@ -168,8 +176,11 @@ files:
168
176
  - lib/stationery/railtie.rb
169
177
  - lib/stationery/regions.rb
170
178
  - lib/stationery/resources.rb
179
+ - lib/stationery/rich/nesting.rb
171
180
  - lib/stationery/rich/nodes.rb
172
181
  - lib/stationery/rich/renderer.rb
182
+ - lib/stationery/rich/renderer/boxes.rb
183
+ - lib/stationery/rich/renderer/indents.rb
173
184
  - lib/stationery/rich/renderer/inlines.rb
174
185
  - lib/stationery/rich/renderer/links.rb
175
186
  - lib/stationery/rich/styles.rb
@@ -184,11 +195,9 @@ files:
184
195
  - lib/stationery/svg/parser.rb
185
196
  - lib/stationery/svg/path_data.rb
186
197
  - lib/stationery/svg/region.rb
187
- - lib/stationery/svg/selector.rb
188
198
  - lib/stationery/svg/shading.rb
189
199
  - lib/stationery/svg/shapes.rb
190
200
  - lib/stationery/svg/style.rb
191
- - lib/stationery/svg/stylesheet.rb
192
201
  - lib/stationery/svg/text.rb
193
202
  - lib/stationery/svg/transform.rb
194
203
  - lib/stationery/svg/viewport.rb
@@ -200,6 +209,7 @@ files:
200
209
  - lib/stationery/testing/marked_text.rb
201
210
  - lib/stationery/testing/matchers.rb
202
211
  - lib/stationery/testing/structure_reader.rb
212
+ - lib/stationery/text/breaks.rb
203
213
  - lib/stationery/text/entities.rb
204
214
  - lib/stationery/text/entities/html4.rb
205
215
  - lib/stationery/text/line.rb