ghostwriter 0.4.2 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +308 -80
- data/RELEASE_NOTES.md +65 -0
- data/dirt-textify.gemspec +8 -6
- data/lib/ghostwriter/version.rb +1 -1
- data/lib/ghostwriter/writer.rb +105 -41
- metadata +16 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ccebf53b35ad212e0c7e353a3cb4d79e1f91c63cec6490c5fff7ff56b72eed70
|
4
|
+
data.tar.gz: 5b3d36839dcef24d80605421970eb39e17efe3ec24d3126d753ac7a5039512a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d6039d9d2b3c1d6606da533c108c8087faa8b985ac0bf6adac7bc1ad4310cc601f76840ff32e83255847bedbd77dbc6cc280054f6eb4975dbe815a98b2a07373
|
7
|
+
data.tar.gz: d005669ca03f3ff465c351df0e47eba0372ca6061057102e14b0d879ddb0c5191a88bfaa389fa82a865787e6ff3b5d3fb351ae8b6351f108aef404a3bd66dd7a
|
data/README.md
CHANGED
@@ -1,14 +1,19 @@
|
|
1
1
|
# Ghostwriter
|
2
2
|
|
3
|
-
|
3
|
+
A ruby gem that converts HTML to plain text, preserving as much legibility and functionality as possible.
|
4
4
|
|
5
|
-
It's sort of like a reverse-markdown.
|
5
|
+
It's sort of like a reverse-markdown or a *very* simple screen reader.
|
6
6
|
|
7
7
|
## But Why, Though?
|
8
8
|
|
9
|
-
*
|
10
|
-
* Some
|
11
|
-
*
|
9
|
+
* Some email clients won't or can’t offer HTML support.
|
10
|
+
* Some people explicitly choose plaintext for accessibility or just plain preference.
|
11
|
+
* Spam filters tend to prefer emails with a plain text alternative (but if you use this gem to spam people,
|
12
|
+
not only might you be
|
13
|
+
[breaking](https://fightspam.gc.ca)
|
14
|
+
[various](https://gdpr.eu/)
|
15
|
+
[laws](https://www.ftc.gov/tips-advice/business-center/guidance/can-spam-act-compliance-guide-business),
|
16
|
+
I will also personally curse you)
|
12
17
|
|
13
18
|
## Installation
|
14
19
|
|
@@ -28,111 +33,234 @@ Or install it manually with:
|
|
28
33
|
|
29
34
|
## Usage
|
30
35
|
|
31
|
-
Create a `Ghostwriter::Writer` with the html you want modified
|
36
|
+
Create a `Ghostwriter::Writer` and call `#textify` with the html string you want modified:
|
32
37
|
|
33
38
|
```ruby
|
34
|
-
html =
|
39
|
+
html = <<~HTML
|
40
|
+
<html>
|
41
|
+
<body>
|
42
|
+
<p>This is some text with <a href="tenjin.ca">a link</a></p>
|
43
|
+
<p>It handles other stuff, too.</p>
|
44
|
+
<hr>
|
45
|
+
<h1>Stuff Like</h1>
|
46
|
+
<ul>
|
47
|
+
<li>Images</li>
|
48
|
+
<li>Lists</li>
|
49
|
+
<li>Tables</li>
|
50
|
+
<li>And more</li>
|
51
|
+
</ul>
|
52
|
+
</body>
|
53
|
+
</html>
|
54
|
+
HTML
|
55
|
+
|
56
|
+
ghostwriter = Ghostwriter::Writer.new
|
35
57
|
|
36
|
-
|
58
|
+
puts ghostwriter.textify(html)
|
37
59
|
```
|
60
|
+
|
38
61
|
Produces:
|
62
|
+
|
39
63
|
```
|
40
|
-
This is some
|
64
|
+
This is some text with a link (tenjin.ca)
|
41
65
|
|
42
|
-
|
66
|
+
It handles other stuff, too.
|
67
|
+
|
68
|
+
|
69
|
+
----------
|
70
|
+
|
71
|
+
-- Stuff Like --
|
72
|
+
- Images
|
73
|
+
- Lists
|
74
|
+
- Tables
|
75
|
+
- And more
|
43
76
|
```
|
44
77
|
|
45
78
|
### Links
|
46
79
|
|
47
80
|
Links are converted to the link text followed by the link target in brackets:
|
48
81
|
|
49
|
-
```
|
50
|
-
|
51
|
-
Ghostwriter::Writer.new(html).textify
|
82
|
+
```html
|
83
|
+
Visit our <a href="https://example.com">Website</a>
|
52
84
|
```
|
53
85
|
|
54
|
-
|
86
|
+
Becomes:
|
87
|
+
|
55
88
|
```
|
56
89
|
Visit our Website (https://example.com)
|
57
90
|
```
|
58
91
|
|
59
92
|
#### Relative Links
|
93
|
+
|
60
94
|
Since emails are wholly distinct from your web address, relative links might break.
|
61
95
|
|
62
96
|
To avoid this problem, either use the `<base>` header tag:
|
63
97
|
|
64
|
-
```
|
65
|
-
html = <<~HTML
|
66
|
-
<html>
|
67
|
-
<head>
|
68
|
-
<base href="https://www.example.com/">
|
69
|
-
</head>
|
70
|
-
<body>
|
71
|
-
Relative links get <a href="/contact">expanded</a> using the head's base tag.
|
72
|
-
</body>
|
73
|
-
</html>
|
74
|
-
HTML
|
98
|
+
```html
|
75
99
|
|
76
|
-
|
100
|
+
<html>
|
101
|
+
<head>
|
102
|
+
<base href="https://www.example.com">
|
103
|
+
</head>
|
104
|
+
<body>
|
105
|
+
Use the base tag to <a href="/contact">expand</a> links.
|
106
|
+
</body>
|
107
|
+
</html>
|
77
108
|
```
|
78
|
-
|
109
|
+
|
110
|
+
Becomes:
|
111
|
+
|
79
112
|
```
|
80
|
-
|
113
|
+
Use the base tag to expand (https://www.example.com/contact) links.
|
81
114
|
```
|
82
115
|
|
83
|
-
Or you can use the `link_base`
|
116
|
+
Or you can use the `link_base` configuration:
|
117
|
+
|
84
118
|
```ruby
|
85
|
-
|
119
|
+
Ghostwriter::Writer.new(link_base: 'tenjin.ca').textify(html)
|
120
|
+
```
|
121
|
+
|
122
|
+
### Images
|
86
123
|
|
87
|
-
|
124
|
+
Images with alt text are converted:
|
125
|
+
|
126
|
+
```html
|
127
|
+
<img src="logo.jpg" alt="ACME Anvils" />
|
88
128
|
```
|
89
129
|
|
90
|
-
|
130
|
+
Becomes:
|
131
|
+
|
91
132
|
```
|
92
|
-
|
133
|
+
ACME Anvils (logo.jpg)
|
93
134
|
```
|
94
135
|
|
95
|
-
|
96
|
-
Tables are often used email structuring because support for more modern CSS is inconsistent.
|
136
|
+
But images lacking alt text or with a presentation ARIA role are ignored:
|
97
137
|
|
98
|
-
|
138
|
+
```html
|
139
|
+
<!-- these will just become an empty string -->
|
140
|
+
<img src="decoration.jpg">
|
141
|
+
<img src="logo.jpg" role="presentation">
|
142
|
+
```
|
99
143
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
<head>
|
104
|
-
<base href="https://www.example.com/">
|
105
|
-
</head>
|
106
|
-
<body>
|
107
|
-
<table>
|
108
|
-
<thead>
|
109
|
-
<tr>
|
110
|
-
<th>Ship</th>
|
111
|
-
<th>Captain</th>
|
112
|
-
</tr>
|
113
|
-
</thead>
|
114
|
-
<tbody>
|
115
|
-
<tr>
|
116
|
-
<td>Enterprise</td>
|
117
|
-
<td>Jean-Luc Picard</td>
|
118
|
-
</tr>
|
119
|
-
<tr>
|
120
|
-
<td>TARDIS</td>
|
121
|
-
<td>The Doctor</td>
|
122
|
-
</tr>
|
123
|
-
<tr>
|
124
|
-
<td>Planet Express Ship</td>
|
125
|
-
<td>Turanga Leela</td>
|
126
|
-
</tr>
|
127
|
-
</tbody>
|
128
|
-
</table>
|
129
|
-
</body>
|
130
|
-
</html>
|
131
|
-
HTML
|
144
|
+
And images with data URIs won't include the data portion.
|
145
|
+
|
146
|
+
```html
|
132
147
|
|
133
|
-
|
148
|
+
<img src="data:image/gif;base64,R0lGODdhIwAjAMZ/AAkMBxETEBUUDBoaExkaGCIcFx4fGCEfFCcfECkjHiUlHiglGikmFjAqFi8pJCsrJT8sCjMzLDUzJzs0GjkzLTszKTM1Mzg4MD48Mzs+O0tAIElCJ1NCGVdBHUtEMkNFQjlHTFJDOkdGPT1ISUxLRENOT1tMI01PTGdLKk1RU0hTVEtTT0NVVFRTTExYWE9YVGhVP1VZXGFYTWhaMFRcWHFYL1FdXV1dRHdZMVRgYFhgXFdiY11hY1tkX31hJltmZ2pnWnloLGFrbG9oYXlqN3NqTnBqWHxqRItvRIh0Nod0ToF2U5J4LX55Xm97e4B5aZqAQpGAdqOCOZKEYZ2FOJyEVoyKbqiOXpySbLCVcLCXaKWbdKCdfZyhi66dksGdc76fbbije7mkdLOmgq6ogrCpibyvirexisWvhs2vgsGyiLq1lce1lMC5ks28nsfBmcHDq9bAl9PDmMnFo9TGh8zIoM7Jm9vLs9nRo93QqtfSquLQpdXUs+fdterlw////ywAAAAAIwAjAAAH/oArOTo6PYaGOz08P0KMOTZCOzw7PzY/Pz2JPYSDhTSFPTSXPY0tIiIfJz05o5Q/O7A5moc6O4Q0oS8uQisXGCItwTItP5OxOrKjhzSfLzYvgz85ERQXJKcSIkZeJDqOl43StrSEKzo2LhkOGBISDw40JyIVFVEyorBCkZmwtCsrtnLQSJCAwoMFCiwoiECPAr0TjPrtECJwXLMVNARlUCBhQAEFC2SsgWPGDBs3d2RcorSD1SVGr3qskOkihoIH70DO0cOHDx48evD0KQONmQ0aORZJE3VLRYoPBRwoUCCCSx07eoL+xLNnj5UfNFry4BHuR6EcK0qkKJFhAYUE/g+cdHlz1efPrnvM2MjhQlYOWTxktXThIoUKhQoKDHBi5Y0dO0CD5smzJ46NvWJfjYW1w4WKEiWkKkgw9UYdPXTo8Mn6042bvX9pTHoFa5GKzykekP5owEidN1u6PKnzMw+QJ3ttUPr7qKUs0C5KHOyoAMMaNWrmjKlSRYscMFm+nBBUybkLSYsIl3DxwAgcKwWMzGnz5kqTK1e09AEDI0uGE8rJEgNfsuxVggoujGABF1xMoYAVc9RRhxxq5JGVHn3EEYcIGfT1igvGKLfDZyWMkMINa5QhQRNz9CQhT1n5URmHJ8Sygw2BSWLDbaCpgEFPNzxBV4QwApVhHBhg/vABZ0pJIhuCoI0wQhFlkLEGGWfQ9wZ2W6KRBhoUJKncKyK2tMOBPI6wwAxltInlG1uKcQUUV3xpwQUXACSJjbCAxgJoJShggBVtnmGGlm/M4UYcX14QQQQ1PpJjUjmsd5sKCg5gBRdkYMlGG2KwoUYWWYARxgXVnODXqmP9CWgJIESwxhJTbEHGGGbMsSWpaRRBQQQXpPKIiJOgg+BnI4AwwhxcHFHrGGN0KYYYaEhAzQX/7flIDMqx4CoIJY7QxhpY0GorXXXwkUcRj1Lg7gfMDavcCSx4BqsIHpyxRhtT1FCDEmNgF4YY1j6KZ4eXXTast9GVcAIHG2TZRhlT/qCAAg5IZIzCA+1QQ0EGKbgAG7c0pPOAAgQcwEQSZ2R5RhlYVIFEFVccAQEAAASgWEIrXEZYDDHQYAEBAQSAcxBUbCExGWVsMfMVCHSA89QCbHBDX4QRRsPURuMcQBBQYLHGHGuwoYUYVdQQxAIOBCCACVLUgDMBS7rwwgtENHDAAEYLMIAAHhABRRVYKFEDDjjU0AA9HiQhxQQOCDC1BXe/UAQVVATRwAIDDGCAAAd0EAQTTEgBBQ4IIFSBFHFPdYEIFJBAQOUE1K5AAyZgnsQME/jNwAG/e7QBFT4sYEABBiQv6ANDDLDCCwPULr0ADYyeOQcMLMAAAxNAIQUHJwckYEDn5CfvgAEKvECA3+R7nrwB2k+ggQkmaLB3++Sz3zkMIawQCAA7"
|
149
|
+
alt="Data picture" />
|
134
150
|
```
|
135
|
-
|
151
|
+
|
152
|
+
Becomes:
|
153
|
+
|
154
|
+
```
|
155
|
+
Data picture (embedded)
|
156
|
+
```
|
157
|
+
|
158
|
+
### Paragraphs and Linebreaks
|
159
|
+
|
160
|
+
Paragraphs are padded with a newline at the end. Line break tags add an empty line.
|
161
|
+
|
162
|
+
```html
|
163
|
+
<p>I would like to propose a toast.</p>
|
164
|
+
<p>This meal we enjoy together would be improved by one.</p>
|
165
|
+
<br />
|
166
|
+
<p>... Plug in the toaster and I'll get the bread.</p>
|
167
|
+
```
|
168
|
+
|
169
|
+
```
|
170
|
+
I would like to propose a toast.
|
171
|
+
|
172
|
+
This meal we enjoy together would be improved by one.
|
173
|
+
|
174
|
+
|
175
|
+
... Plug in the toaster and I'll get the bread.
|
176
|
+
|
177
|
+
```
|
178
|
+
|
179
|
+
### Headings
|
180
|
+
|
181
|
+
Headings are wrapped with a marker per heading level:
|
182
|
+
|
183
|
+
```html
|
184
|
+
<h1>Dog Maintenance and Repair</h1>
|
185
|
+
<h2>Food Input Port</h2>
|
186
|
+
<h3>Exhaust Port Considerations</h3>
|
187
|
+
```
|
188
|
+
|
189
|
+
Becomes:
|
190
|
+
|
191
|
+
```
|
192
|
+
-- Dog Maintenance and Repair --
|
193
|
+
---- Food Input Port ----
|
194
|
+
------ Exhaust Port Considerations ------
|
195
|
+
```
|
196
|
+
|
197
|
+
The `<header>` tag is treated like an `<h1>` tag.
|
198
|
+
|
199
|
+
### Lists
|
200
|
+
|
201
|
+
Lists are converted, too. They are padded with newlines and are given simple markers:
|
202
|
+
|
203
|
+
```html
|
204
|
+
|
205
|
+
<ul>
|
206
|
+
<li>Planes</li>
|
207
|
+
<li>Trains</li>
|
208
|
+
<li>Automobiles</li>
|
209
|
+
</ul>
|
210
|
+
<ol>
|
211
|
+
<li>I get knocked down</li>
|
212
|
+
<li>I get up again</li>
|
213
|
+
<li>Never gonna keep me down</li>
|
214
|
+
</ol>
|
215
|
+
```
|
216
|
+
|
217
|
+
Becomes:
|
218
|
+
|
219
|
+
```
|
220
|
+
- Planes
|
221
|
+
- Trains
|
222
|
+
- Automobiles
|
223
|
+
|
224
|
+
1. I get knocked down
|
225
|
+
2. I get up again
|
226
|
+
3. Never gonna keep me down
|
227
|
+
```
|
228
|
+
|
229
|
+
### Tables
|
230
|
+
|
231
|
+
Tables are still often used in email structuring because support for more modern HTML and CSS is inconsistent. If your
|
232
|
+
table is purely presentational, mark it with `role="presentation"`. See below for details.
|
233
|
+
|
234
|
+
For real data tables, Ghostwriter tries to maintain table structure for simple tables:
|
235
|
+
|
236
|
+
```html
|
237
|
+
|
238
|
+
<table>
|
239
|
+
<thead>
|
240
|
+
<tr>
|
241
|
+
<th>Ship</th>
|
242
|
+
<th>Captain</th>
|
243
|
+
</tr>
|
244
|
+
</thead>
|
245
|
+
<tbody>
|
246
|
+
<tr>
|
247
|
+
<td>Enterprise</td>
|
248
|
+
<td>Jean-Luc Picard</td>
|
249
|
+
</tr>
|
250
|
+
<tr>
|
251
|
+
<td>TARDIS</td>
|
252
|
+
<td>The Doctor</td>
|
253
|
+
</tr>
|
254
|
+
<tr>
|
255
|
+
<td>Planet Express Ship</td>
|
256
|
+
<td>Turanga Leela</td>
|
257
|
+
</tr>
|
258
|
+
</tbody>
|
259
|
+
</table>
|
260
|
+
```
|
261
|
+
|
262
|
+
Becomes:
|
263
|
+
|
136
264
|
```
|
137
265
|
| Ship | Captain |
|
138
266
|
|---------------------|-----------------|
|
@@ -141,6 +269,105 @@ Produces:
|
|
141
269
|
| Planet Express Ship | Turanga Leela |
|
142
270
|
```
|
143
271
|
|
272
|
+
### Customizing Output
|
273
|
+
|
274
|
+
Ghostwriter has some constructor options to customize output.
|
275
|
+
|
276
|
+
You can set heading markers.
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
html = <<~HTML
|
280
|
+
<h1>Emergency Cat Procedures</h1>
|
281
|
+
HTML
|
282
|
+
|
283
|
+
writer = Ghostwriter::Writer.new(heading_marker: '#')
|
284
|
+
|
285
|
+
puts writer.textify(html)
|
286
|
+
```
|
287
|
+
|
288
|
+
Produces:
|
289
|
+
|
290
|
+
```
|
291
|
+
# Emergency Cat Procedures #
|
292
|
+
```
|
293
|
+
|
294
|
+
You can also set list item markers. Ordered markers can be anything that responds to `#next` (eg. any `Enumerator`)
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
html = <<~HTML
|
298
|
+
<ol><li>Mercury</li><li>Venus</li><li>Mars</li></ol>
|
299
|
+
<ul><li>Teapot</li><li>Kettle</li></ul>
|
300
|
+
HTML
|
301
|
+
|
302
|
+
writer = Ghostwriter::Writer.new(ul_marker: '*', ol_marker: 'a')
|
303
|
+
|
304
|
+
puts writer.textify(html)
|
305
|
+
```
|
306
|
+
|
307
|
+
Produces:
|
308
|
+
|
309
|
+
```
|
310
|
+
a. Mercury
|
311
|
+
b. Venus
|
312
|
+
c. Mars
|
313
|
+
|
314
|
+
* Teapot
|
315
|
+
* Kettle
|
316
|
+
```
|
317
|
+
|
318
|
+
And tables can be customized:
|
319
|
+
|
320
|
+
```ruby
|
321
|
+
writer = Ghostwriter::Writer.new(table_row: '.',
|
322
|
+
table_column: '#',
|
323
|
+
table_corner: '+')
|
324
|
+
|
325
|
+
puts writer.textify <<~HTML
|
326
|
+
<table>
|
327
|
+
<thead>
|
328
|
+
<tr><th>Moon</th><th>Portfolio</th></tr>
|
329
|
+
</thead>
|
330
|
+
<tbody>
|
331
|
+
<tr><td>Phobos</td><td>Fear & Panic</td></tr>
|
332
|
+
<tr><td>Deimos</td><td>Dread and Terror</td></tr>
|
333
|
+
</tbody>
|
334
|
+
</table>
|
335
|
+
HTML
|
336
|
+
```
|
337
|
+
|
338
|
+
Produces:
|
339
|
+
|
340
|
+
```
|
341
|
+
# Moon # Portfolio #
|
342
|
+
+........+..................+
|
343
|
+
# Phobos # Fear & Panic #
|
344
|
+
# Deimos # Dread and Terror #
|
345
|
+
|
346
|
+
```
|
347
|
+
|
348
|
+
#### Presentation ARIA Role
|
349
|
+
|
350
|
+
Tags with `role="presentation"` will be treated as a simple container and the normal behaviour will be suppressed.
|
351
|
+
|
352
|
+
```html
|
353
|
+
|
354
|
+
<table role="presentation">
|
355
|
+
<tr>
|
356
|
+
<td>The table is a lie</td>
|
357
|
+
</tr>
|
358
|
+
</table>
|
359
|
+
<ul role="presentation">
|
360
|
+
<li>No such list</li>
|
361
|
+
</ul>
|
362
|
+
```
|
363
|
+
|
364
|
+
Becomes:
|
365
|
+
|
366
|
+
```
|
367
|
+
The table is a lie
|
368
|
+
No such list
|
369
|
+
```
|
370
|
+
|
144
371
|
### Mail Gem Example
|
145
372
|
|
146
373
|
To use `#textify` with the [mail](https://github.com/mikel/mail) gem, just provide the text-part by pasisng the html
|
@@ -149,7 +376,8 @@ through Ghostwriter:
|
|
149
376
|
```ruby
|
150
377
|
require 'mail'
|
151
378
|
|
152
|
-
html
|
379
|
+
html = 'My email and a <a href="https://tenjin.ca">link</a>'
|
380
|
+
ghostwriter = Ghostwriter::Writer.new
|
153
381
|
|
154
382
|
Mail.deliver do
|
155
383
|
to 'bob@example.com'
|
@@ -162,7 +390,7 @@ Mail.deliver do
|
|
162
390
|
end
|
163
391
|
|
164
392
|
text_part do
|
165
|
-
body
|
393
|
+
body ghostwriter.textify(html)
|
166
394
|
end
|
167
395
|
end
|
168
396
|
|
@@ -181,19 +409,19 @@ After checking out the repo, run `bundle install` to install dependencies. Then,
|
|
181
409
|
can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
182
410
|
|
183
411
|
#### Local Install
|
184
|
-
To install this gem onto your local machine only, run
|
185
412
|
|
186
|
-
|
413
|
+
To install this gem onto your local machine only, run
|
414
|
+
|
415
|
+
`bundle exec rake install`
|
187
416
|
|
188
417
|
#### Gem Release
|
418
|
+
|
189
419
|
To release a gem to the world at large
|
190
420
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
196
|
-
3. Do a wee dance
|
421
|
+
1. Update the version number in `version.rb`,
|
422
|
+
2. Run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push
|
423
|
+
the `.gem` file to [rubygems.org](https://rubygems.org).
|
424
|
+
3. Do a wee dance
|
197
425
|
|
198
426
|
## License
|
199
427
|
|
data/RELEASE_NOTES.md
CHANGED
@@ -1,5 +1,70 @@
|
|
1
1
|
# Release Notes
|
2
2
|
|
3
|
+
## 1.2.1 (2021-10-29)
|
4
|
+
|
5
|
+
### Major
|
6
|
+
|
7
|
+
* none
|
8
|
+
|
9
|
+
### Minor
|
10
|
+
|
11
|
+
* Updated Nokogiri version to resolve https://github.com/advisories/GHSA-7rrm-v45f-jp64
|
12
|
+
* Updated Ruby version dependency to match
|
13
|
+
* Relaxed dependency upper bounds
|
14
|
+
|
15
|
+
### Bugfixes
|
16
|
+
|
17
|
+
* none
|
18
|
+
|
19
|
+
## 1.1.0 (2021-03-23)
|
20
|
+
|
21
|
+
### Major
|
22
|
+
|
23
|
+
* none
|
24
|
+
|
25
|
+
### Minor
|
26
|
+
|
27
|
+
* Added customization for headings
|
28
|
+
* Headings now marked more for higher order headings
|
29
|
+
* Added customization for list markers
|
30
|
+
* Added customization for table markers
|
31
|
+
* Writer is now immutable
|
32
|
+
|
33
|
+
### Bugfixes
|
34
|
+
|
35
|
+
* none
|
36
|
+
|
37
|
+
## 1.0.1 (2021-03-22)
|
38
|
+
|
39
|
+
### Major
|
40
|
+
|
41
|
+
* none
|
42
|
+
|
43
|
+
### Minor
|
44
|
+
|
45
|
+
* Updated README
|
46
|
+
|
47
|
+
### Bugfixes
|
48
|
+
|
49
|
+
* Fixed hr padding behaviour
|
50
|
+
|
51
|
+
## 1.0.0 (2021-03-21)
|
52
|
+
|
53
|
+
### Major
|
54
|
+
|
55
|
+
* Moved `link_base` parameter to constructor
|
56
|
+
* Moved input HTML parameter to `#textify`
|
57
|
+
|
58
|
+
### Minor
|
59
|
+
|
60
|
+
* Treats tables and lists with role="presentation" as simple containers
|
61
|
+
* Now handles ordered and unordered lists
|
62
|
+
* Images are now replaced with their alt text
|
63
|
+
|
64
|
+
### Bugfixes
|
65
|
+
|
66
|
+
* none
|
67
|
+
|
3
68
|
## 0.4.2 (2021-03-17)
|
4
69
|
|
5
70
|
### Major
|
data/dirt-textify.gemspec
CHANGED
@@ -10,9 +10,11 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.authors = ['Robin Miller']
|
11
11
|
spec.email = ['robin@tenjin.ca']
|
12
12
|
|
13
|
-
spec.summary = '
|
13
|
+
spec.summary = 'Converts HTML to plain text'
|
14
14
|
spec.description = <<~DESC
|
15
|
-
|
15
|
+
Converts HTML to plain text, preserving as much legibility and functionality as possible.
|
16
|
+
|
17
|
+
Ideal for providing a plaintext multipart segment of email messages.
|
16
18
|
DESC
|
17
19
|
spec.homepage = 'https://github.com/TenjinInc/ghostwriter'
|
18
20
|
spec.license = 'MIT'
|
@@ -25,13 +27,13 @@ Gem::Specification.new do |spec|
|
|
25
27
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
28
|
spec.require_paths = ['lib']
|
27
29
|
|
28
|
-
spec.required_ruby_version = '
|
30
|
+
spec.required_ruby_version = '>= 2.7'
|
29
31
|
|
30
|
-
spec.add_dependency 'nokogiri', '
|
32
|
+
spec.add_dependency 'nokogiri', '>= 1.12'
|
31
33
|
|
32
34
|
spec.add_development_dependency 'bundler', '~> 2.2'
|
33
35
|
spec.add_development_dependency 'rake', '~> 13.0'
|
34
36
|
spec.add_development_dependency 'rspec', '~> 3.3'
|
35
|
-
spec.add_development_dependency 'rubocop', '~> 1.
|
36
|
-
spec.add_development_dependency 'rubocop-performance', '~> 1.
|
37
|
+
spec.add_development_dependency 'rubocop', '~> 1.22'
|
38
|
+
spec.add_development_dependency 'rubocop-performance', '~> 1.11'
|
37
39
|
end
|
data/lib/ghostwriter/version.rb
CHANGED
data/lib/ghostwriter/writer.rb
CHANGED
@@ -3,52 +3,59 @@
|
|
3
3
|
module Ghostwriter
|
4
4
|
# Main Ghostwriter converter object.
|
5
5
|
class Writer
|
6
|
-
|
7
|
-
|
6
|
+
attr_reader :link_base, :heading_marker, :ul_marker, :ol_marker, :table_row, :table_column, :table_corner
|
7
|
+
|
8
|
+
# Creates a new ghostwriter
|
9
|
+
#
|
10
|
+
# @param [String] link_base the url to prefix relative links with
|
11
|
+
def initialize(link_base: '', heading_marker: '--', ul_marker: '-', ol_marker: '1',
|
12
|
+
table_column: '|', table_row: '-', table_corner: '|')
|
13
|
+
@link_base = link_base
|
14
|
+
@heading_marker = heading_marker
|
15
|
+
@ul_marker = ul_marker
|
16
|
+
@ol_marker = ol_marker
|
17
|
+
@table_column = table_column
|
18
|
+
@table_row = table_row
|
19
|
+
@table_corner = table_corner
|
20
|
+
|
21
|
+
freeze
|
8
22
|
end
|
9
23
|
|
10
24
|
# Strips HTML down to plain text.
|
11
25
|
#
|
12
|
-
# @param
|
13
|
-
|
14
|
-
|
26
|
+
# @param html [String] the HTML to be convert to text
|
27
|
+
#
|
28
|
+
# @return converted text
|
29
|
+
def textify(html)
|
30
|
+
doc = Nokogiri::HTML(html.gsub(/\s+/, ' '))
|
31
|
+
|
32
|
+
doc.search('style, script').remove
|
15
33
|
|
16
|
-
doc
|
34
|
+
replace_anchors(doc)
|
35
|
+
replace_images(doc)
|
17
36
|
|
18
|
-
doc
|
19
|
-
doc.search('script').remove
|
37
|
+
simple_replace(doc, '*[role="presentation"]', "\n")
|
20
38
|
|
21
|
-
replace_anchors(doc, link_base)
|
22
39
|
replace_headers(doc)
|
40
|
+
replace_lists(doc)
|
23
41
|
replace_tables(doc)
|
24
42
|
|
25
|
-
simple_replace(doc, 'hr', "\n----------\n")
|
43
|
+
simple_replace(doc, 'hr', "\n----------\n\n")
|
26
44
|
simple_replace(doc, 'br', "\n")
|
45
|
+
simple_replace(doc, 'p', "\n\n")
|
27
46
|
|
28
|
-
|
29
|
-
# link_node.inner_html = link_node.inner_html + "\n\n"
|
30
|
-
# end
|
31
|
-
|
32
|
-
# trim, but only single-space character
|
33
|
-
doc.text.gsub(/^ +| +$/, '')
|
47
|
+
normalize_lines(doc)
|
34
48
|
end
|
35
49
|
|
36
50
|
private
|
37
51
|
|
38
|
-
def
|
39
|
-
|
52
|
+
def normalize_lines(doc)
|
53
|
+
doc.text.strip.split("\n").collect(&:strip).join("\n").concat("\n")
|
40
54
|
end
|
41
55
|
|
42
|
-
def replace_anchors(doc
|
43
|
-
base = get_link_base(doc, default: link_base)
|
44
|
-
|
56
|
+
def replace_anchors(doc)
|
45
57
|
doc.search('a').each do |link_node|
|
46
|
-
|
47
|
-
href = URI(link_node['href'])
|
48
|
-
href = base + href.to_s unless href.absolute?
|
49
|
-
rescue URI::InvalidURIError
|
50
|
-
href = link_node['href'].gsub(/^(tel|mailto):/, '').strip
|
51
|
-
end
|
58
|
+
href = get_link_target(link_node, get_link_base(doc))
|
52
59
|
|
53
60
|
link_node.inner_html = if link_matches(href, link_node.inner_html)
|
54
61
|
href.to_s
|
@@ -62,39 +69,96 @@ module Ghostwriter
|
|
62
69
|
first.to_s.gsub(%r{^https?://}, '').chomp('/') == second.gsub(%r{^https?://}, '').chomp('/')
|
63
70
|
end
|
64
71
|
|
65
|
-
def get_link_base(doc
|
72
|
+
def get_link_base(doc)
|
66
73
|
# <base> node is unique by W3C spec
|
67
74
|
base_node = doc.search('base').first
|
68
75
|
|
69
|
-
base_node ? base_node['href'] :
|
76
|
+
base_node ? base_node['href'] : @link_base
|
77
|
+
end
|
78
|
+
|
79
|
+
def get_link_target(link_node, base)
|
80
|
+
href = URI(link_node['href'])
|
81
|
+
if href.absolute?
|
82
|
+
href
|
83
|
+
else
|
84
|
+
base + href.to_s
|
85
|
+
end
|
86
|
+
rescue URI::InvalidURIError
|
87
|
+
link_node['href'].gsub(/^(tel|mailto):/, '').strip
|
70
88
|
end
|
71
89
|
|
72
90
|
def replace_headers(doc)
|
73
|
-
doc.search('header, h1
|
74
|
-
node.
|
91
|
+
doc.search('header, h1').each do |node|
|
92
|
+
node.replace("#{ @heading_marker } #{ node.inner_html } #{ @heading_marker }\n"
|
93
|
+
.squeeze(' '))
|
94
|
+
end
|
95
|
+
|
96
|
+
(2..6).each do |n|
|
97
|
+
doc.search("h#{ n }").each do |node|
|
98
|
+
node.replace("#{ @heading_marker * n } #{ node.inner_html } #{ @heading_marker * n }\n"
|
99
|
+
.squeeze(' '))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def replace_images(doc)
|
105
|
+
doc.search('img[role=presentation]').remove
|
106
|
+
|
107
|
+
doc.search('img').each do |img_node|
|
108
|
+
src = img_node['src']
|
109
|
+
alt = img_node['alt']
|
110
|
+
|
111
|
+
src = 'embedded' if src.start_with? 'data:'
|
112
|
+
|
113
|
+
img_node.replace("#{ alt } (#{ src })") unless alt.nil? || alt.empty?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def replace_lists(doc)
|
118
|
+
doc.search('ol').each do |list_node|
|
119
|
+
replace_list_items(list_node, @ol_marker, after_marker: '.', increment: true)
|
120
|
+
end
|
121
|
+
|
122
|
+
doc.search('ul').each do |list_node|
|
123
|
+
replace_list_items(list_node, @ul_marker)
|
124
|
+
end
|
125
|
+
|
126
|
+
doc.search('ul, ol').each do |list_node|
|
127
|
+
list_node.replace("#{ list_node.inner_html }\n")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def replace_list_items(list_node, marker, after_marker: '', increment: false)
|
132
|
+
list_node.search('./li').each do |list_item|
|
133
|
+
list_item.replace("#{ marker }#{ after_marker } #{ list_item.inner_html }\n")
|
134
|
+
|
135
|
+
marker = marker.next if increment
|
75
136
|
end
|
76
137
|
end
|
77
138
|
|
78
139
|
def replace_tables(doc)
|
79
140
|
doc.css('table').each do |table|
|
141
|
+
# remove whitespace between nodes
|
142
|
+
table.search('//text()[normalize-space()=""]').remove
|
143
|
+
|
80
144
|
column_sizes = calculate_column_sizes(table)
|
81
145
|
|
82
146
|
table.search('./thead/tr', './tbody/tr', './tr').each do |row|
|
83
147
|
replace_table_nodes(row, column_sizes)
|
84
148
|
|
85
|
-
row.
|
149
|
+
row.replace("#{ row.inner_html }#{ @table_column }\n")
|
86
150
|
end
|
87
151
|
|
88
152
|
add_table_header_underline(table, column_sizes)
|
89
153
|
|
90
|
-
table.
|
154
|
+
table.replace("\n#{ table.inner_html }\n")
|
91
155
|
end
|
92
156
|
end
|
93
157
|
|
94
158
|
def calculate_column_sizes(table)
|
95
159
|
column_sizes = table.search('tr').collect do |row|
|
96
160
|
row.search('th', 'td').collect do |node|
|
97
|
-
node.
|
161
|
+
node.text.length
|
98
162
|
end
|
99
163
|
end
|
100
164
|
|
@@ -102,25 +166,25 @@ module Ghostwriter
|
|
102
166
|
end
|
103
167
|
|
104
168
|
def add_table_header_underline(table, column_sizes)
|
105
|
-
table.search('./thead').each do |
|
106
|
-
|
169
|
+
table.search('./thead').each do |thead|
|
170
|
+
lines = column_sizes.collect { |len| @table_row * (len + 2) }
|
171
|
+
underline_row = "#{ table_corner }#{ lines.join(@table_corner) }#{ @table_corner }"
|
107
172
|
|
108
|
-
|
173
|
+
thead.replace("#{ thead.inner_html }#{ underline_row }\n")
|
109
174
|
end
|
110
175
|
end
|
111
176
|
|
112
177
|
def replace_table_nodes(row, column_sizes)
|
113
178
|
row.search('th', 'td').each_with_index do |node, i|
|
114
|
-
new_content =
|
179
|
+
new_content = node.text.ljust(column_sizes[i] + 1)
|
115
180
|
|
116
|
-
#
|
117
|
-
node.inner_html = new_content.ljust(column_sizes[i] + 2)
|
181
|
+
node.replace("#{ @table_column } #{ new_content }")
|
118
182
|
end
|
119
183
|
end
|
120
184
|
|
121
185
|
def simple_replace(doc, tag, replacement)
|
122
186
|
doc.search(tag).each do |node|
|
123
|
-
node.replace(replacement)
|
187
|
+
node.replace(node.inner_html + replacement)
|
124
188
|
end
|
125
189
|
end
|
126
190
|
end
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ghostwriter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robin Miller
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-10-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 1.
|
19
|
+
version: '1.12'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 1.
|
26
|
+
version: '1.12'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -72,31 +72,32 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '1.
|
75
|
+
version: '1.22'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '1.
|
82
|
+
version: '1.22'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: rubocop-performance
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '1.
|
89
|
+
version: '1.11'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '1.
|
97
|
-
description:
|
96
|
+
version: '1.11'
|
97
|
+
description: |
|
98
|
+
Converts HTML to plain text, preserving as much legibility and functionality as possible.
|
98
99
|
|
99
|
-
|
100
|
+
Ideal for providing a plaintext multipart segment of email messages.
|
100
101
|
email:
|
101
102
|
- robin@tenjin.ca
|
102
103
|
executables: []
|
@@ -130,9 +131,9 @@ require_paths:
|
|
130
131
|
- lib
|
131
132
|
required_ruby_version: !ruby/object:Gem::Requirement
|
132
133
|
requirements:
|
133
|
-
- - "
|
134
|
+
- - ">="
|
134
135
|
- !ruby/object:Gem::Version
|
135
|
-
version: '2.
|
136
|
+
version: '2.7'
|
136
137
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
138
|
requirements:
|
138
139
|
- - ">="
|
@@ -142,5 +143,5 @@ requirements: []
|
|
142
143
|
rubygems_version: 3.1.2
|
143
144
|
signing_key:
|
144
145
|
specification_version: 4
|
145
|
-
summary:
|
146
|
+
summary: Converts HTML to plain text
|
146
147
|
test_files: []
|