bandoola 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +42 -5
- data/lib/bandoola/standard_metrics.rb +33 -13
- data/lib/bandoola/version.rb +1 -1
- data/lib/bandoola/view/element.rb +48 -7
- data/lib/bandoola/view/header.rb +22 -5
- data/lib/bandoola/view/inline.rb +188 -0
- data/lib/bandoola/view/resources.rb +14 -0
- data/lib/bandoola/view/style/typography.rb +31 -1
- data/lib/bandoola/view/style.rb +2 -0
- data/lib/bandoola/view.rb +37 -6
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca546c7de64a2889f63c5d6263add435f60097dabd9b5a7f7019dcd411993eb6
|
|
4
|
+
data.tar.gz: aac0858803151d55f36cd237278ca8e54bf82b7a098f760749ff0b5fc4b19ff0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8985871cdc3266c7a2ed27a00488e1faab5757c79e558725acad21f15705af21f2de4b208d85cf7c4b1783082961d614241bff473d8605a3e609f8d262e70b01
|
|
7
|
+
data.tar.gz: 97b668f1390c7b01cef9575c93c9d865d40bc20b3ce76d8472bccc5a5d8bc879ef3b61ebb1ab7d2a25518102443b8584a2fc442a7900a529ed53f231c4d9e9f6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-09-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `span` element: inline text. Consecutive spans flow together as one wrapped
|
|
8
|
+
paragraph, each with its own font, weight, size and color.
|
|
9
|
+
- `a(href:)` element: an inline, clickable link (a PDF `/Link` annotation with a
|
|
10
|
+
URI action). Wrapped links are clickable on every line. Unstyled by default.
|
|
11
|
+
- `underline` / `no-underline` classes for `text`, `span` and `a`.
|
|
12
|
+
- Line height: `leading-none`, `leading-tight`, `leading-snug`,
|
|
13
|
+
`leading-normal`, `leading-relaxed`, `leading-loose` (relative to the text
|
|
14
|
+
size) and `leading-3` … `leading-10` (fixed). Inherited from a `div`, like CSS.
|
|
15
|
+
- Border variants: bare `border`, and per-edge `border-t/-r/-b/-l/-x/-y`,
|
|
16
|
+
optionally with a width (`border-b-2`). Partial borders are straight edges.
|
|
17
|
+
- SVG masks: `<mask>` definitions (their `<path>`/`<rect>` children) clip the
|
|
18
|
+
paths or groups that reference them via `mask="url(#id)"`.
|
|
19
|
+
- Tests and samples covering every font weight from `font-thin` to `font-black`.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The extra space a line height adds is now split evenly above and below the
|
|
24
|
+
text, as in CSS, instead of all going below. Text at the default size sits 2pt
|
|
25
|
+
lower in its box than in 0.1.0; box sizes are unchanged.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Standard-font widths for WinAnsi's 0x80–0x9F characters (— – “ ” ‘ ’ € … •
|
|
30
|
+
™ Š Ž Œ and the rest). They used to be measured at the width of "?", so
|
|
31
|
+
wrapping and alignment were off and text after one of them could overlap the
|
|
32
|
+
next span.
|
|
33
|
+
- Standard-font widths of the straight apostrophe `'` and the backtick, which
|
|
34
|
+
used Adobe StandardEncoding's curly-quote widths instead of WinAnsi's glyphs.
|
|
35
|
+
|
|
3
36
|
## [0.1.0] - 2026-06-16
|
|
4
37
|
|
|
5
38
|
- Initial release
|
data/README.md
CHANGED
|
@@ -32,7 +32,14 @@ space-separated string or an array of tokens) and, where it makes sense, a block
|
|
|
32
32
|
- `div(class:) { … }` — a block-level box that lays out the elements inside it.
|
|
33
33
|
- `text(class:) { "…" }` — a run of text; the block returns the string. It wraps
|
|
34
34
|
to its box width, and embedded newlines start new lines.
|
|
35
|
-
- `
|
|
35
|
+
- `span(class:) { "…" }` — inline text. Consecutive spans flow together as one
|
|
36
|
+
wrapped paragraph, each with its own weight, size and color:
|
|
37
|
+
`div { span { "This is " }; span(class: "font-bold") { "cool" } }`.
|
|
38
|
+
- `a(href:, class:) { "…" }` — a clickable link to `href`. It flows inline like
|
|
39
|
+
`span` and is unstyled by default, so add e.g. `class: "underline text-blue-800"`.
|
|
40
|
+
- `img(src:, class:)` — an image; `src` is a path to an SVG or JPEG. SVG support
|
|
41
|
+
covers solid-filled `<path>`s, `<g>` groups and `<mask>` clipping — enough for
|
|
42
|
+
icons and logos, but no gradients, strokes or text.
|
|
36
43
|
|
|
37
44
|
Elements flow like HTML blocks: each fills its container's width and stacks below
|
|
38
45
|
the previous one, unless a width class makes it share a row (and reflow).
|
|
@@ -46,22 +53,52 @@ for the full scales. What Bandoola understands today:
|
|
|
46
53
|
| --- | --- |
|
|
47
54
|
| Padding / margin | `p-4`, `px-2`, `pt-1`, `m-4`, `mx-2`, … |
|
|
48
55
|
| Width / height | `w-32`, `w-1/2`, `w-full`, `h-16`, `h-1/2`, `h-full` |
|
|
49
|
-
| Border width | `border-1` … `border-16` |
|
|
56
|
+
| Border width | `border`, `border-1` … `border-16`; per edge `border-t`, `-r`, `-b`, `-l`, `-x`, `-y` (e.g. `border-b-2`) |
|
|
50
57
|
| Border radius | `rounded`, `rounded-sm` … `rounded-3xl`, `rounded-full` |
|
|
51
58
|
| Background | `bg-cyan-100`, `bg-slate-800`, `bg-black`, `bg-white` |
|
|
52
59
|
| Border color | `border-blue-600` (needs a border width to show) |
|
|
53
60
|
| Text color | `text-rose-500`, `text-white` |
|
|
54
61
|
| Font family | `font-sans`, `font-serif`, `font-mono`, `font-<registered>` |
|
|
55
|
-
| Font weight | `font-light`
|
|
62
|
+
| Font weight | `font-thin`, `font-extralight`, `font-light`, `font-normal`, `font-medium`, `font-semibold`, `font-bold`, `font-extrabold`, `font-black` |
|
|
56
63
|
| Text size | `text-xs` … `text-9xl` |
|
|
64
|
+
| Text decoration | `underline`, `no-underline` |
|
|
65
|
+
| Line height | `leading-none`, `-tight`, `-snug`, `-normal`, `-relaxed`, `-loose`; `leading-3` … `leading-10` |
|
|
57
66
|
|
|
58
67
|
Sizes and spacing are points (1px ≈ 1pt). Notes:
|
|
59
68
|
|
|
60
69
|
- `div` has no fill or border by default — add `bg-*`, `border-*`, or `rounded-*`.
|
|
61
70
|
- Colors work on the full Tailwind palette plus `black`/`white`; a color with
|
|
62
71
|
no shade (`bg-cyan`) uses 500.
|
|
63
|
-
- A `div`'s `font-*` family and weight are inherited by the
|
|
64
|
-
size is set per `text` element.
|
|
72
|
+
- A `div`'s `font-*` family and weight and its `leading-*` are inherited by the
|
|
73
|
+
text inside it; text size is set per `text` element.
|
|
74
|
+
- `leading-tight` etc. are multiples of each element's own text size, while
|
|
75
|
+
`leading-6` is a fixed height (24pt). As in CSS, the extra space is split evenly
|
|
76
|
+
above and below the text.
|
|
77
|
+
- The standard fonts only have regular and bold faces: weights below normal
|
|
78
|
+
render as normal, and in-between or heavier weights use the nearest face plus a
|
|
79
|
+
synthetic (faux-bold) stroke. Register a file per weight (`thin:`, `black:`, …)
|
|
80
|
+
to get real faces.
|
|
81
|
+
- Spans ignore padding, margin and alignment classes.
|
|
82
|
+
|
|
83
|
+
## Inline text & links
|
|
84
|
+
|
|
85
|
+
`span` and `a` flow inline: consecutive ones form a single paragraph that wraps
|
|
86
|
+
across them, so each piece can have its own styling. A `text`, `div` or `img`
|
|
87
|
+
between them starts a new paragraph.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
div(class: "leading-relaxed") do
|
|
91
|
+
span { "Bandoola is " }
|
|
92
|
+
span(class: "font-bold") { "cool " }
|
|
93
|
+
span { "and you can read " }
|
|
94
|
+
a(class: "underline text-blue-800", href: "https://www.example.com") { "the docs" }
|
|
95
|
+
span { "." }
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Links become clickable areas in the PDF that open their `href`. Like in
|
|
100
|
+
Tailwind, they have no styling by default, so give them a color and `underline`.
|
|
101
|
+
Line height (`leading-*`) set on the `div` applies to every span in it.
|
|
65
102
|
|
|
66
103
|
## Fonts
|
|
67
104
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
module Bandoola
|
|
2
2
|
class StandardFont
|
|
3
|
-
# Adobe Core font advance widths (1000-unit em) by
|
|
4
|
-
#
|
|
5
|
-
#
|
|
3
|
+
# Adobe Core font advance widths (1000-unit em) by Unicode code point, for
|
|
4
|
+
# the standard PDF faces families map to. Used to measure and wrap text.
|
|
5
|
+
# They cover every WinAnsi character: ASCII, Latin-1 (160-255) and the
|
|
6
|
+
# 0x80-0x9F extras (curly quotes, dashes, €, …), which sit at their Unicode
|
|
7
|
+
# code points (U+2014 for the em dash). Codes 39 and 96 are WinAnsi's
|
|
8
|
+
# quotesingle and grave, not Adobe StandardEncoding's curly quotes.
|
|
9
|
+
# Courier is monospace, handled in code.
|
|
6
10
|
METRICS = {
|
|
7
11
|
"Helvetica" => {
|
|
8
|
-
32 => 278, 33 => 278, 34 => 355, 35 => 556, 36 => 556, 37 => 889, 38 => 667, 39 =>
|
|
12
|
+
32 => 278, 33 => 278, 34 => 355, 35 => 556, 36 => 556, 37 => 889, 38 => 667, 39 => 191,
|
|
9
13
|
40 => 333, 41 => 333, 42 => 389, 43 => 584, 44 => 278, 45 => 333, 46 => 278, 47 => 278,
|
|
10
14
|
48 => 556, 49 => 556, 50 => 556, 51 => 556, 52 => 556, 53 => 556, 54 => 556, 55 => 556,
|
|
11
15
|
56 => 556, 57 => 556, 58 => 278, 59 => 278, 60 => 584, 61 => 584, 62 => 584, 63 => 556,
|
|
@@ -13,7 +17,7 @@ module Bandoola
|
|
|
13
17
|
72 => 722, 73 => 278, 74 => 500, 75 => 667, 76 => 556, 77 => 833, 78 => 722, 79 => 778,
|
|
14
18
|
80 => 667, 81 => 778, 82 => 722, 83 => 667, 84 => 611, 85 => 722, 86 => 667, 87 => 944,
|
|
15
19
|
88 => 667, 89 => 667, 90 => 611, 91 => 278, 92 => 278, 93 => 278, 94 => 469, 95 => 556,
|
|
16
|
-
96 =>
|
|
20
|
+
96 => 333, 97 => 556, 98 => 556, 99 => 500, 100 => 556, 101 => 556, 102 => 278, 103 => 556,
|
|
17
21
|
104 => 556, 105 => 222, 106 => 222, 107 => 500, 108 => 222, 109 => 833, 110 => 556, 111 => 556,
|
|
18
22
|
112 => 556, 113 => 556, 114 => 333, 115 => 500, 116 => 278, 117 => 556, 118 => 500, 119 => 722,
|
|
19
23
|
120 => 500, 121 => 500, 122 => 500, 123 => 334, 124 => 260, 125 => 334, 126 => 584, 160 => 278,
|
|
@@ -28,10 +32,14 @@ module Bandoola
|
|
|
28
32
|
225 => 556, 226 => 556, 227 => 556, 228 => 556, 229 => 556, 230 => 889, 231 => 500, 232 => 556,
|
|
29
33
|
233 => 556, 234 => 556, 235 => 556, 236 => 278, 237 => 278, 238 => 278, 239 => 278, 240 => 556,
|
|
30
34
|
241 => 556, 242 => 556, 243 => 556, 244 => 556, 245 => 556, 246 => 556, 247 => 584, 248 => 611,
|
|
31
|
-
249 => 556, 250 => 556, 251 => 556, 252 => 556, 253 => 500, 254 => 556, 255 => 500
|
|
35
|
+
249 => 556, 250 => 556, 251 => 556, 252 => 556, 253 => 500, 254 => 556, 255 => 500, 338 => 1000,
|
|
36
|
+
339 => 944, 352 => 667, 353 => 500, 376 => 667, 381 => 611, 382 => 500, 402 => 556, 710 => 333,
|
|
37
|
+
732 => 333, 8211 => 556, 8212 => 1000, 8216 => 222, 8217 => 222, 8218 => 222, 8220 => 333, 8221 => 333,
|
|
38
|
+
8222 => 333, 8224 => 556, 8225 => 556, 8226 => 350, 8230 => 1000, 8240 => 1000, 8249 => 333, 8250 => 333,
|
|
39
|
+
8364 => 556, 8482 => 1000
|
|
32
40
|
},
|
|
33
41
|
"Helvetica-Bold" => {
|
|
34
|
-
32 => 278, 33 => 333, 34 => 474, 35 => 556, 36 => 556, 37 => 889, 38 => 722, 39 =>
|
|
42
|
+
32 => 278, 33 => 333, 34 => 474, 35 => 556, 36 => 556, 37 => 889, 38 => 722, 39 => 238,
|
|
35
43
|
40 => 333, 41 => 333, 42 => 389, 43 => 584, 44 => 278, 45 => 333, 46 => 278, 47 => 278,
|
|
36
44
|
48 => 556, 49 => 556, 50 => 556, 51 => 556, 52 => 556, 53 => 556, 54 => 556, 55 => 556,
|
|
37
45
|
56 => 556, 57 => 556, 58 => 333, 59 => 333, 60 => 584, 61 => 584, 62 => 584, 63 => 611,
|
|
@@ -39,7 +47,7 @@ module Bandoola
|
|
|
39
47
|
72 => 722, 73 => 278, 74 => 556, 75 => 722, 76 => 611, 77 => 833, 78 => 722, 79 => 778,
|
|
40
48
|
80 => 667, 81 => 778, 82 => 722, 83 => 667, 84 => 611, 85 => 722, 86 => 667, 87 => 944,
|
|
41
49
|
88 => 667, 89 => 667, 90 => 611, 91 => 333, 92 => 278, 93 => 333, 94 => 584, 95 => 556,
|
|
42
|
-
96 =>
|
|
50
|
+
96 => 333, 97 => 556, 98 => 611, 99 => 556, 100 => 611, 101 => 556, 102 => 333, 103 => 611,
|
|
43
51
|
104 => 611, 105 => 278, 106 => 278, 107 => 556, 108 => 278, 109 => 889, 110 => 611, 111 => 611,
|
|
44
52
|
112 => 611, 113 => 611, 114 => 389, 115 => 556, 116 => 333, 117 => 611, 118 => 556, 119 => 778,
|
|
45
53
|
120 => 556, 121 => 556, 122 => 500, 123 => 389, 124 => 280, 125 => 389, 126 => 584, 160 => 278,
|
|
@@ -54,10 +62,14 @@ module Bandoola
|
|
|
54
62
|
225 => 556, 226 => 556, 227 => 556, 228 => 556, 229 => 556, 230 => 889, 231 => 556, 232 => 556,
|
|
55
63
|
233 => 556, 234 => 556, 235 => 556, 236 => 278, 237 => 278, 238 => 278, 239 => 278, 240 => 611,
|
|
56
64
|
241 => 611, 242 => 611, 243 => 611, 244 => 611, 245 => 611, 246 => 611, 247 => 584, 248 => 611,
|
|
57
|
-
249 => 611, 250 => 611, 251 => 611, 252 => 611, 253 => 556, 254 => 611, 255 => 556
|
|
65
|
+
249 => 611, 250 => 611, 251 => 611, 252 => 611, 253 => 556, 254 => 611, 255 => 556, 338 => 1000,
|
|
66
|
+
339 => 944, 352 => 667, 353 => 556, 376 => 667, 381 => 611, 382 => 500, 402 => 556, 710 => 333,
|
|
67
|
+
732 => 333, 8211 => 556, 8212 => 1000, 8216 => 278, 8217 => 278, 8218 => 278, 8220 => 500, 8221 => 500,
|
|
68
|
+
8222 => 500, 8224 => 556, 8225 => 556, 8226 => 350, 8230 => 1000, 8240 => 1000, 8249 => 333, 8250 => 333,
|
|
69
|
+
8364 => 556, 8482 => 1000
|
|
58
70
|
},
|
|
59
71
|
"Times-Roman" => {
|
|
60
|
-
32 => 250, 33 => 333, 34 => 408, 35 => 500, 36 => 500, 37 => 833, 38 => 778, 39 =>
|
|
72
|
+
32 => 250, 33 => 333, 34 => 408, 35 => 500, 36 => 500, 37 => 833, 38 => 778, 39 => 180,
|
|
61
73
|
40 => 333, 41 => 333, 42 => 500, 43 => 564, 44 => 250, 45 => 333, 46 => 250, 47 => 278,
|
|
62
74
|
48 => 500, 49 => 500, 50 => 500, 51 => 500, 52 => 500, 53 => 500, 54 => 500, 55 => 500,
|
|
63
75
|
56 => 500, 57 => 500, 58 => 278, 59 => 278, 60 => 564, 61 => 564, 62 => 564, 63 => 444,
|
|
@@ -80,10 +92,14 @@ module Bandoola
|
|
|
80
92
|
225 => 444, 226 => 444, 227 => 444, 228 => 444, 229 => 444, 230 => 667, 231 => 444, 232 => 444,
|
|
81
93
|
233 => 444, 234 => 444, 235 => 444, 236 => 278, 237 => 278, 238 => 278, 239 => 278, 240 => 500,
|
|
82
94
|
241 => 500, 242 => 500, 243 => 500, 244 => 500, 245 => 500, 246 => 500, 247 => 564, 248 => 500,
|
|
83
|
-
249 => 500, 250 => 500, 251 => 500, 252 => 500, 253 => 500, 254 => 500, 255 => 500
|
|
95
|
+
249 => 500, 250 => 500, 251 => 500, 252 => 500, 253 => 500, 254 => 500, 255 => 500, 338 => 889,
|
|
96
|
+
339 => 722, 352 => 556, 353 => 389, 376 => 722, 381 => 611, 382 => 444, 402 => 500, 710 => 333,
|
|
97
|
+
732 => 333, 8211 => 500, 8212 => 1000, 8216 => 333, 8217 => 333, 8218 => 333, 8220 => 444, 8221 => 444,
|
|
98
|
+
8222 => 444, 8224 => 500, 8225 => 500, 8226 => 350, 8230 => 1000, 8240 => 1000, 8249 => 333, 8250 => 333,
|
|
99
|
+
8364 => 500, 8482 => 980
|
|
84
100
|
},
|
|
85
101
|
"Times-Bold" => {
|
|
86
|
-
32 => 250, 33 => 333, 34 => 555, 35 => 500, 36 => 500, 37 => 1000, 38 => 833, 39 =>
|
|
102
|
+
32 => 250, 33 => 333, 34 => 555, 35 => 500, 36 => 500, 37 => 1000, 38 => 833, 39 => 278,
|
|
87
103
|
40 => 333, 41 => 333, 42 => 500, 43 => 570, 44 => 250, 45 => 333, 46 => 250, 47 => 278,
|
|
88
104
|
48 => 500, 49 => 500, 50 => 500, 51 => 500, 52 => 500, 53 => 500, 54 => 500, 55 => 500,
|
|
89
105
|
56 => 500, 57 => 500, 58 => 333, 59 => 333, 60 => 570, 61 => 570, 62 => 570, 63 => 500,
|
|
@@ -106,7 +122,11 @@ module Bandoola
|
|
|
106
122
|
225 => 500, 226 => 500, 227 => 500, 228 => 500, 229 => 500, 230 => 722, 231 => 444, 232 => 444,
|
|
107
123
|
233 => 444, 234 => 444, 235 => 444, 236 => 278, 237 => 278, 238 => 278, 239 => 278, 240 => 500,
|
|
108
124
|
241 => 556, 242 => 500, 243 => 500, 244 => 500, 245 => 500, 246 => 500, 247 => 570, 248 => 500,
|
|
109
|
-
249 => 556, 250 => 556, 251 => 556, 252 => 556, 253 => 500, 254 => 556, 255 => 500
|
|
125
|
+
249 => 556, 250 => 556, 251 => 556, 252 => 556, 253 => 500, 254 => 556, 255 => 500, 338 => 1000,
|
|
126
|
+
339 => 722, 352 => 556, 353 => 389, 376 => 722, 381 => 667, 382 => 444, 402 => 500, 710 => 333,
|
|
127
|
+
732 => 333, 8211 => 500, 8212 => 1000, 8216 => 333, 8217 => 333, 8218 => 333, 8220 => 500, 8221 => 500,
|
|
128
|
+
8222 => 500, 8224 => 500, 8225 => 500, 8226 => 350, 8230 => 1000, 8240 => 1000, 8249 => 333, 8250 => 333,
|
|
129
|
+
8364 => 500, 8482 => 1000
|
|
110
130
|
}
|
|
111
131
|
}.freeze
|
|
112
132
|
end
|
data/lib/bandoola/version.rb
CHANGED
|
@@ -270,6 +270,11 @@ module Bandoola
|
|
|
270
270
|
# the font size. Tuned so font-bold (700) reads clearly bold.
|
|
271
271
|
BOLD_STEP = 0.008
|
|
272
272
|
|
|
273
|
+
# The underline's gap below the baseline and its thickness, as fractions
|
|
274
|
+
# of the font size.
|
|
275
|
+
UNDERLINE_OFFSET = 0.1
|
|
276
|
+
UNDERLINE_THICKNESS = 0.06
|
|
277
|
+
|
|
273
278
|
attr_reader :content
|
|
274
279
|
|
|
275
280
|
def initialize(style, content)
|
|
@@ -280,6 +285,7 @@ module Bandoola
|
|
|
280
285
|
# Wrap the text to the available width (using +typeset+'s measurer and the
|
|
281
286
|
# effective font), storing the lines for paint, and return their height.
|
|
282
287
|
def content_height(_left, _top, avail, _avail_height, typeset)
|
|
288
|
+
@line_height = line_height_for(typeset.leading)
|
|
283
289
|
@lines = wrap(avail, typeset)
|
|
284
290
|
@lines.length * line_height
|
|
285
291
|
end
|
|
@@ -296,17 +302,33 @@ module Bandoola
|
|
|
296
302
|
|
|
297
303
|
buffer << "q\n#{setup}" if setup
|
|
298
304
|
wrapped.each_with_index do |line, index|
|
|
299
|
-
baseline = content_top - font_size - (index * line_height)
|
|
305
|
+
baseline = content_top - half_leading - font_size - (index * line_height)
|
|
300
306
|
indent = align_indent(line, content_width, face)
|
|
301
|
-
|
|
302
|
-
buffer <<
|
|
303
|
-
"#{coord(left + indent)} #{coord(baseline)} Td\n#{face.encode(line)} Tj\nET\n"
|
|
307
|
+
buffer << show(name, face, line, left + indent, baseline)
|
|
308
|
+
buffer << underline(left + indent, baseline, face.width_of(line, font_size))
|
|
304
309
|
end
|
|
305
310
|
buffer << "Q\n" if setup
|
|
306
311
|
end
|
|
307
312
|
|
|
308
313
|
private
|
|
309
314
|
|
|
315
|
+
# A text object drawing +line+ at (left, baseline). The font encodes the
|
|
316
|
+
# text (WinAnsi literal or Identity-H glyph ids).
|
|
317
|
+
def show(name, face, line, left, baseline)
|
|
318
|
+
"BT\n/#{name} #{font_size} Tf\n#{coord(left)} #{coord(baseline)} Td\n#{face.encode(line)} Tj\nET\n"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# With the underline class, a thin filled rule under a run +width+ wide,
|
|
322
|
+
# in the text color; otherwise nothing.
|
|
323
|
+
def underline(left, baseline, width)
|
|
324
|
+
return "" unless style.underline
|
|
325
|
+
|
|
326
|
+
thickness = font_size * UNDERLINE_THICKNESS
|
|
327
|
+
bottom = baseline - (font_size * UNDERLINE_OFFSET) - thickness
|
|
328
|
+
"q\n#{color_op(style.text_color || [0.0, 0.0, 0.0])}" \
|
|
329
|
+
"#{coord(left)} #{coord(bottom)} #{coord(width)} #{coord(thickness)} re\nf\nQ\n"
|
|
330
|
+
end
|
|
331
|
+
|
|
310
332
|
# Horizontal offset for a line under the text-align class: 0 for the
|
|
311
333
|
# default left, the leftover width for right, half of it for center.
|
|
312
334
|
def align_indent(line, content_width, face)
|
|
@@ -361,10 +383,29 @@ module Bandoola
|
|
|
361
383
|
style.text_size || FONT_SIZE
|
|
362
384
|
end
|
|
363
385
|
|
|
364
|
-
#
|
|
365
|
-
#
|
|
386
|
+
# The line height decided during layout (from a leading-* class, this
|
|
387
|
+
# element's or inherited), else the default.
|
|
366
388
|
def line_height
|
|
367
|
-
|
|
389
|
+
@line_height || line_height_for(nil)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# The line height under this element's leading, or +inherited+ when it
|
|
393
|
+
# has none. A ratio applies to this element's own font size (like a
|
|
394
|
+
# unitless CSS line-height); with no leading at all it's the default
|
|
395
|
+
# proportion (LINE_HEIGHT for the default FONT_SIZE).
|
|
396
|
+
def line_height_for(inherited)
|
|
397
|
+
kind, value = style.leading || inherited
|
|
398
|
+
case kind
|
|
399
|
+
when :ratio then font_size * value
|
|
400
|
+
when :fixed then value
|
|
401
|
+
else (font_size * LINE_HEIGHT / FONT_SIZE.to_f).round
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# As in CSS, the space a line height adds beyond the font size is split
|
|
406
|
+
# evenly above and below the glyphs; this is the share above.
|
|
407
|
+
def half_leading
|
|
408
|
+
(line_height - font_size) / 2.0
|
|
368
409
|
end
|
|
369
410
|
|
|
370
411
|
# The graphics-state setup for a text run: a fill color (text-<color>)
|
data/lib/bandoola/view/header.rb
CHANGED
|
@@ -38,8 +38,8 @@ module Bandoola
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# The document's objects, in order: catalog (1), pages (2), the N Page
|
|
41
|
-
# objects, the N content streams, then a block per used font
|
|
42
|
-
# XObject per used image. Object numbers are 1-based array positions.
|
|
41
|
+
# objects, the N content streams, then a block per used font, one
|
|
42
|
+
# XObject per used image and one annotation per link. Object numbers are 1-based array positions.
|
|
43
43
|
def build_objects
|
|
44
44
|
pages = @page_buffers.size
|
|
45
45
|
first_page = 3
|
|
@@ -60,7 +60,14 @@ module Bandoola
|
|
|
60
60
|
number += 1
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
annot_refs = @page_links.map do |links|
|
|
64
|
+
links.map do |rect, uri|
|
|
65
|
+
extras << link_annotation(rect, uri)
|
|
66
|
+
(number += 1) - 1
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
page_objects = Array.new(pages) { |i| page_object(first_content + i, annot_refs[i]) }
|
|
64
71
|
content_objects = @page_buffers.map { |buffer| content_object(buffer) }
|
|
65
72
|
|
|
66
73
|
[catalog, pages_object(first_page, pages, font_refs, image_refs), *page_objects, *content_objects, *extras]
|
|
@@ -79,8 +86,18 @@ module Bandoola
|
|
|
79
86
|
"/MediaBox [0 0 #{width} #{height}] /Resources << #{resources} >> >>"
|
|
80
87
|
end
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
# A page lists its link annotations (by object number) in /Annots.
|
|
90
|
+
def page_object(content_number, annot_numbers)
|
|
91
|
+
annots = annot_numbers.empty? ? "" : " /Annots [#{annot_numbers.map { |n| "#{n} 0 R" }.join(" ")}]"
|
|
92
|
+
"<< /Type /Page /Parent 2 0 R /Contents #{content_number} 0 R#{annots} >>"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# A clickable area ([left, bottom, right, top]) that opens +uri+. No
|
|
96
|
+
# border, so it's invisible; the text's own styling marks it as a link.
|
|
97
|
+
def link_annotation(rect, uri)
|
|
98
|
+
corners = rect.map { |value| format("%.2f", value).sub(/\.?0+\z/, "") }.join(" ")
|
|
99
|
+
escaped = uri.gsub(/[\\()]/) { |char| "\\#{char}" }
|
|
100
|
+
"<< /Type /Annot /Subtype /Link /Rect [#{corners}] /Border [0 0 0] /A << /S /URI /URI (#{escaped}) >> >>"
|
|
84
101
|
end
|
|
85
102
|
|
|
86
103
|
def content_object(buffer)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
module Bandoola
|
|
2
|
+
class View
|
|
3
|
+
# A piece of inline text. Spans never lay themselves out; the InlineRun
|
|
4
|
+
# holding them breaks their words into lines and asks each span to measure
|
|
5
|
+
# and draw its share. A span's own font-*/weight classes win over the
|
|
6
|
+
# inherited ones, like Text; text-<size> and text-<color> apply per span.
|
|
7
|
+
# Padding, margin and alignment classes are ignored on spans.
|
|
8
|
+
class Span < Text
|
|
9
|
+
def family_for(font) = style.font || font
|
|
10
|
+
def weight_for(weight) = style.font_weight || weight
|
|
11
|
+
def size = font_size
|
|
12
|
+
# This span's line height under the inherited +typeset+ leading.
|
|
13
|
+
def leading(typeset) = line_height_for(typeset.leading)
|
|
14
|
+
|
|
15
|
+
# Width of +string+ in this span's face; 0 without a measurer.
|
|
16
|
+
def width_of(string, typeset)
|
|
17
|
+
return 0.0 unless typeset.measure
|
|
18
|
+
|
|
19
|
+
typeset.measure.call(string, family_for(typeset.font), weight_for(typeset.weight), size)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Draw +string+ with its baseline starting at +origin+ ([x, y]), in the
|
|
23
|
+
# face resolved from the inherited [font, weight].
|
|
24
|
+
def paint_piece(buffer, resources, string, origin, inherited)
|
|
25
|
+
font, weight = inherited
|
|
26
|
+
name, native, face = resources.font(family_for(font), weight_for(weight))
|
|
27
|
+
setup = text_state(weight_for(weight) - native)
|
|
28
|
+
|
|
29
|
+
width = face.width_of(string, size)
|
|
30
|
+
|
|
31
|
+
buffer << "q\n#{setup}" if setup
|
|
32
|
+
buffer << show(name, face, string, *origin)
|
|
33
|
+
buffer << "Q\n" if setup
|
|
34
|
+
buffer << underline(*origin, width)
|
|
35
|
+
annotate(resources, origin, width)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# A hook for spans that attach something to the page where they're drawn.
|
|
41
|
+
def annotate(_resources, _origin, _width); end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The a element: a span that is also a link. Each piece it draws gets a
|
|
45
|
+
# clickable area on its page pointing at +href+ (a URI).
|
|
46
|
+
class A < Span
|
|
47
|
+
# How far the clickable area reaches below the baseline and above it, as
|
|
48
|
+
# fractions of the font size — roughly the glyphs' descent and ascent.
|
|
49
|
+
DESCENT = 0.25
|
|
50
|
+
ASCENT = 0.9
|
|
51
|
+
|
|
52
|
+
attr_reader :href
|
|
53
|
+
|
|
54
|
+
def initialize(style, content, href)
|
|
55
|
+
super(style, content)
|
|
56
|
+
@href = href
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def annotate(resources, origin, width)
|
|
62
|
+
left, baseline = origin
|
|
63
|
+
rect = [left, baseline - (size * DESCENT), left + width, baseline + (size * ASCENT)]
|
|
64
|
+
resources.add_link(rect, href)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# An anonymous block that holds consecutive spans and lays them out as one
|
|
69
|
+
# paragraph: a greedy word wrap across span boundaries, where each line is
|
|
70
|
+
# as tall as its tallest span and every span on it shares a baseline. Words
|
|
71
|
+
# break only at whitespace, so "cool" + "er" in two spans stays together.
|
|
72
|
+
class InlineRun < Element
|
|
73
|
+
def initialize
|
|
74
|
+
super(Style.none)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def content_height(_left, _top, avail, _avail_height, typeset)
|
|
78
|
+
@lines = LineBreaker.new(children, avail, typeset).lines
|
|
79
|
+
@lines.sum(&:height)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def paint(buffer, resources, font = "sans", weight = NORMAL_WEIGHT)
|
|
83
|
+
line_top = top
|
|
84
|
+
(@lines || []).each do |line|
|
|
85
|
+
baseline = line_top - line.ascent
|
|
86
|
+
line.pieces.each do |span, text, offset|
|
|
87
|
+
span.paint_piece(buffer, resources, text, [x + offset, baseline], [font, weight])
|
|
88
|
+
end
|
|
89
|
+
line_top -= line.height
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The spans have no geometry of their own; only the run moves.
|
|
94
|
+
def offset(delta)
|
|
95
|
+
@top += delta
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Breaks a run of spans into lines no wider than +avail+.
|
|
100
|
+
class LineBreaker
|
|
101
|
+
# +pieces+ are [span, text, x offset]; +ascent+ is the distance from the
|
|
102
|
+
# line's top to its shared baseline.
|
|
103
|
+
Line = Struct.new(:pieces, :width, :height, :ascent)
|
|
104
|
+
|
|
105
|
+
TOKEN = /\n|[^\S\n]+|\S+/
|
|
106
|
+
|
|
107
|
+
attr_reader :lines
|
|
108
|
+
|
|
109
|
+
def initialize(spans, avail, typeset)
|
|
110
|
+
@spans = spans
|
|
111
|
+
@avail = avail
|
|
112
|
+
@typeset = typeset
|
|
113
|
+
@lines = []
|
|
114
|
+
@line = new_line
|
|
115
|
+
@space = nil # [span, width] of the gap before the next word
|
|
116
|
+
chunks.each { |parts| consume(parts) }
|
|
117
|
+
finish(@spans.last) if @line.pieces.any? || @lines.empty?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
# The spans' content as chunks: a hard break, a space, or a word — the
|
|
123
|
+
# [span, text] parts of adjacent non-space tokens, joined across spans.
|
|
124
|
+
def chunks
|
|
125
|
+
tokens = @spans.flat_map { |span| span.content.scan(TOKEN).map { |text| [span, text] } }
|
|
126
|
+
tokens.chunk_while { |(_, left), (_, right)| word?(left) && word?(right) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def word?(text) = !text.match?(/\s/)
|
|
130
|
+
|
|
131
|
+
def consume(parts)
|
|
132
|
+
span, text = parts.first
|
|
133
|
+
if text == "\n"
|
|
134
|
+
finish(span)
|
|
135
|
+
elsif !word?(text)
|
|
136
|
+
@space = [span, span.width_of(" ", @typeset)] if @line.pieces.any?
|
|
137
|
+
else
|
|
138
|
+
place(parts)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Add a word after the pending space, or start a new line when it would
|
|
143
|
+
# overflow this one.
|
|
144
|
+
def place(parts)
|
|
145
|
+
widths = parts.map { |span, text| span.width_of(text, @typeset) }
|
|
146
|
+
finish(parts.first[0]) if @line.pieces.any? && @line.width + gap + widths.sum > @avail + Element::EPSILON
|
|
147
|
+
|
|
148
|
+
cursor = @line.width + gap
|
|
149
|
+
parts.each_with_index do |(span, text), index|
|
|
150
|
+
if index.zero? && continues?(span)
|
|
151
|
+
@line.pieces.last[1] << " " << text # one run of the same span: draw it in a single Tj
|
|
152
|
+
else
|
|
153
|
+
@line.pieces << [span, +text, cursor]
|
|
154
|
+
end
|
|
155
|
+
cursor += widths[index]
|
|
156
|
+
end
|
|
157
|
+
@line.width = cursor
|
|
158
|
+
@space = nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def gap = @space ? @space[1] : 0.0
|
|
162
|
+
|
|
163
|
+
# Whether +span+'s word joins the previous piece across a space in the same span.
|
|
164
|
+
def continues?(span)
|
|
165
|
+
last = @line.pieces.last
|
|
166
|
+
@space && last && last[0] == span && @space[0] == span
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def new_line = Line.new([], 0.0, 0.0, 0.0)
|
|
170
|
+
|
|
171
|
+
# Close the current line and start a fresh one. Each span (or +fallback+
|
|
172
|
+
# when the line is empty) needs its font size plus half its extra leading
|
|
173
|
+
# above the baseline and the other half below; the line is tall enough
|
|
174
|
+
# for the most demanding span on each side, as in CSS.
|
|
175
|
+
def finish(fallback)
|
|
176
|
+
spans = @line.pieces.map(&:first)
|
|
177
|
+
spans = [fallback] if spans.empty?
|
|
178
|
+
halves = spans.map { |span| (span.leading(@typeset) - span.size) / 2.0 }
|
|
179
|
+
above = spans.zip(halves).map { |span, half| span.size + half }.max
|
|
180
|
+
@line.ascent = above
|
|
181
|
+
@line.height = above + halves.max
|
|
182
|
+
@lines << @line
|
|
183
|
+
@line = new_line
|
|
184
|
+
@space = nil
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -13,6 +13,7 @@ module Bandoola
|
|
|
13
13
|
@font_resolver = font_resolver
|
|
14
14
|
@fonts = {} # face key => [resource_name, font]
|
|
15
15
|
@images = {} # image => [resource_name, image]
|
|
16
|
+
@links = [] # [rect, uri] on the page being painted
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# The used fonts/images as [resource_name, …] pairs, in first-use order.
|
|
@@ -33,6 +34,19 @@ module Bandoola
|
|
|
33
34
|
[entry[0], native, font]
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
# Record a link area ([left, bottom, right, top]) on the current page.
|
|
38
|
+
def add_link(rect, uri)
|
|
39
|
+
@links << [rect, uri]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The links recorded since the last call, i.e. those of the page just
|
|
43
|
+
# painted, leaving the list empty for the next page.
|
|
44
|
+
def take_links
|
|
45
|
+
links = @links
|
|
46
|
+
@links = []
|
|
47
|
+
links
|
|
48
|
+
end
|
|
49
|
+
|
|
36
50
|
# Resource name for an image, deduped so the same image painted more than
|
|
37
51
|
# once (e.g. a logo in a header repeated on every page) embeds once.
|
|
38
52
|
def add_image(image)
|
|
@@ -5,6 +5,8 @@ module Bandoola
|
|
|
5
5
|
# font-sans / font-serif / font-mono / font-<name> -> font family
|
|
6
6
|
# font-light/normal/medium/semibold/bold/... -> font weight
|
|
7
7
|
# text-xs … text-9xl -> font size (pt)
|
|
8
|
+
# underline / no-underline -> text decoration
|
|
9
|
+
# leading-none/tight/…/loose, leading-3 … leading-10 -> line height
|
|
8
10
|
#
|
|
9
11
|
# The family is recorded by name (the View resolves it to an actual font
|
|
10
12
|
# at render time). The weight is a number; since we don't carry separate
|
|
@@ -35,13 +37,41 @@ module Bandoola
|
|
|
35
37
|
|
|
36
38
|
ALIGNMENTS = %w[left center right].freeze
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
# Tailwind's relative line heights, as multiples of the font size.
|
|
41
|
+
# leading-<n> is instead a fixed height on the spacing scale.
|
|
42
|
+
LEADINGS = {
|
|
43
|
+
"none" => 1.0, "tight" => 1.25, "snug" => 1.375, "normal" => 1.5,
|
|
44
|
+
"relaxed" => 1.625, "loose" => 2.0
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
# +leading+ is [:ratio, multiple] or [:fixed, points], or nil.
|
|
48
|
+
attr_reader :font, :font_weight, :text_size, :text_align, :underline, :leading
|
|
39
49
|
|
|
40
50
|
def init_typography
|
|
41
51
|
@font = nil
|
|
42
52
|
@font_weight = nil
|
|
43
53
|
@text_size = nil
|
|
44
54
|
@text_align = nil
|
|
55
|
+
@underline = false
|
|
56
|
+
@leading = nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def apply_leading_token(token)
|
|
60
|
+
return unless token.start_with?("leading-")
|
|
61
|
+
|
|
62
|
+
key = token.delete_prefix("leading-")
|
|
63
|
+
if (ratio = LEADINGS[key])
|
|
64
|
+
@leading = [:ratio, ratio]
|
|
65
|
+
elsif key.match?(/\A\d+\z/) && (points = SCALE[key.to_i])
|
|
66
|
+
@leading = [:fixed, points]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def apply_decoration_token(token)
|
|
71
|
+
case token
|
|
72
|
+
when "underline" then @underline = true
|
|
73
|
+
when "no-underline" then @underline = false
|
|
74
|
+
end
|
|
45
75
|
end
|
|
46
76
|
|
|
47
77
|
def apply_font_token(token)
|
data/lib/bandoola/view/style.rb
CHANGED
data/lib/bandoola/view.rb
CHANGED
|
@@ -2,6 +2,7 @@ require_relative "view/header"
|
|
|
2
2
|
require_relative "view/file"
|
|
3
3
|
require_relative "view/style"
|
|
4
4
|
require_relative "view/element"
|
|
5
|
+
require_relative "view/inline"
|
|
5
6
|
require_relative "view/resources"
|
|
6
7
|
|
|
7
8
|
module Bandoola
|
|
@@ -130,11 +131,11 @@ module Bandoola
|
|
|
130
131
|
|
|
131
132
|
PAGE_BREAK_EPSILON = 0.01
|
|
132
133
|
|
|
133
|
-
# The inherited font/weight and a text measurer, threaded through
|
|
134
|
-
# Text can wrap. #inherit applies a div's
|
|
135
|
-
Typeset = Struct.new(:font, :weight, :measure) do
|
|
134
|
+
# The inherited font/weight/leading and a text measurer, threaded through
|
|
135
|
+
# layout so Text can wrap. #inherit applies a div's classes to its children.
|
|
136
|
+
Typeset = Struct.new(:font, :weight, :measure, :leading) do
|
|
136
137
|
def inherit(style)
|
|
137
|
-
Typeset.new(style.font || font, style.font_weight || weight, measure)
|
|
138
|
+
Typeset.new(style.font || font, style.font_weight || weight, measure, style.leading || leading)
|
|
138
139
|
end
|
|
139
140
|
end
|
|
140
141
|
# Used when a tree is laid out without typesetting (no wrapping).
|
|
@@ -170,14 +171,18 @@ module Bandoola
|
|
|
170
171
|
body.layout(margin, body_top, content_width, body_height, @typeset)
|
|
171
172
|
pages = paginate(body.children, body_height, body_top)
|
|
172
173
|
|
|
173
|
-
|
|
174
|
+
# Each page's buffer, plus the link areas painted onto it.
|
|
175
|
+
@page_buffers = []
|
|
176
|
+
@page_links = []
|
|
177
|
+
pages.each_with_index do |children, index|
|
|
174
178
|
@page_number = index + 1
|
|
175
179
|
@page_count = pages.size
|
|
176
180
|
buffer = String.new(encoding: Encoding::ASCII_8BIT)
|
|
177
181
|
paint_header(buffer)
|
|
178
182
|
paint_footer(buffer)
|
|
179
183
|
children.each { |child| child.paint(buffer, @resources) }
|
|
180
|
-
buffer
|
|
184
|
+
@page_buffers << buffer
|
|
185
|
+
@page_links << @resources.take_links
|
|
181
186
|
end
|
|
182
187
|
|
|
183
188
|
@contents = serialize
|
|
@@ -342,6 +347,23 @@ module Bandoola
|
|
|
342
347
|
nil
|
|
343
348
|
end
|
|
344
349
|
|
|
350
|
+
# The span element: an inline run of text. Consecutive spans flow together
|
|
351
|
+
# as one paragraph, wrapping across span boundaries, so each can carry its
|
|
352
|
+
# own font, weight, size and color:
|
|
353
|
+
#
|
|
354
|
+
# div { span { "This is " } span(class: "font-bold") { "cool" } }
|
|
355
|
+
def span(**attributes)
|
|
356
|
+
content = block_given? ? yield.to_s : ""
|
|
357
|
+
inline(Span.new(Style.parse(attributes[:class]), content))
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# The a element: inline text like span that links to +href+ (a URI) when
|
|
361
|
+
# clicked. It isn't styled by default; add e.g. "underline text-blue-800".
|
|
362
|
+
def a(href:, **attributes)
|
|
363
|
+
content = block_given? ? yield.to_s : ""
|
|
364
|
+
inline(A.new(Style.parse(attributes[:class]), content, href.to_s))
|
|
365
|
+
end
|
|
366
|
+
|
|
345
367
|
# The image element. +src+ is a path to an SVG or JPEG. Sizing follows the
|
|
346
368
|
# width/height classes (see Style); with none it uses the image's own size.
|
|
347
369
|
def img(src:, **attributes)
|
|
@@ -349,6 +371,15 @@ module Bandoola
|
|
|
349
371
|
nil
|
|
350
372
|
end
|
|
351
373
|
|
|
374
|
+
# Add an inline element to the current parent's trailing run of inline
|
|
375
|
+
# text, starting a new run if the previous sibling isn't one.
|
|
376
|
+
def inline(element)
|
|
377
|
+
siblings = @stack.last.children
|
|
378
|
+
siblings << InlineRun.new unless siblings.last.is_a?(InlineRun)
|
|
379
|
+
siblings.last.children << element
|
|
380
|
+
nil
|
|
381
|
+
end
|
|
382
|
+
|
|
352
383
|
# Add +element+ to the current parent, and — if a block is given — make
|
|
353
384
|
# it the parent while the block runs so nested tags land inside it.
|
|
354
385
|
def append(element, &block)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bandoola
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Johan Halse
|
|
@@ -37,6 +37,7 @@ files:
|
|
|
37
37
|
- lib/bandoola/view/element.rb
|
|
38
38
|
- lib/bandoola/view/file.rb
|
|
39
39
|
- lib/bandoola/view/header.rb
|
|
40
|
+
- lib/bandoola/view/inline.rb
|
|
40
41
|
- lib/bandoola/view/resources.rb
|
|
41
42
|
- lib/bandoola/view/style.rb
|
|
42
43
|
- lib/bandoola/view/style/borders.rb
|
|
@@ -67,7 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
67
68
|
- !ruby/object:Gem::Version
|
|
68
69
|
version: '0'
|
|
69
70
|
requirements: []
|
|
70
|
-
rubygems_version: 4.0.
|
|
71
|
+
rubygems_version: 4.0.21
|
|
71
72
|
specification_version: 4
|
|
72
73
|
summary: A small, dependency-free PDF generator for Ruby with Tailwind-style classes.
|
|
73
74
|
test_files: []
|