email_signature_parser 0.1.3 → 0.1.5

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: 1bfc690817d76870045cb565082894471ff91b530c1ca0d29b0f8932a485f3a0
4
- data.tar.gz: e8a0d00b64d50b77c8ea8a43c449e4ae03e6dc7e94d78e6c22bf6ff26c87ab9e
3
+ metadata.gz: 941d06a5998603e510610fde9ba3021c54a40e8d754abca14f211887bf77ecd5
4
+ data.tar.gz: e355a66f5ee272fd8957742f262dee201a9e0db25b1b150d1dffdc991d635236
5
5
  SHA512:
6
- metadata.gz: 0ff3f082a7d0366b6953db51ab0139fe5482ca2216c773f255547f4c51634946c3419e112c70a70cbfcfaba51e5f492312013aca4527050d063005deef73d95f
7
- data.tar.gz: 1069582d064cff2b58898f5bdb01a86d6478e0f38a20c2cb63437382871624502c5062c3cf84ea9444b8771dbbfb2ba8dd217b51cc90c8636825301cb1baeff9
6
+ metadata.gz: b584c051aef0fc2fce5b1806f18f004db6f2df6fa85cd2968a0374dbcfc0d0b6488735b726b9fe2fb32eb31386685a115fd7b3cd981f7506841822644238ca37
7
+ data.tar.gz: d08534fbe76ea16383b6a76fbe27a02bb64ed6cf3244dca7c47de35e40d8cde8113701976e28d34383bfd0f6e2583a4066804f77a194d90d4020cf121a8d970d
@@ -0,0 +1,506 @@
1
+ module EmailSignatureParser
2
+ class HtmlToMarkdownParser < ::Ox::Sax
3
+ attr_reader :parsed_text
4
+ HTML_SPECIAL_ESCAPED_CHARACTERS = {
5
+ '&lt;' => '<',
6
+ '&gt;' => '>',
7
+ }
8
+
9
+ HTML_SPECIAL_CHARACTERS = {
10
+ # Reserved
11
+ '&amp;' => '&',
12
+ '&quot;' => '"',
13
+ '&apos;' => "'",
14
+
15
+ # ISO 8859-1 Symbols
16
+ '&nbsp;' => ' ',
17
+ '&iexcl;' => '¡',
18
+ '&cent;' => '¢',
19
+ '&pound;' => '£',
20
+ '&curren;' => '¤',
21
+ '&yen;' => '¥',
22
+ '&brvbar;' => '¦',
23
+ '&sect;' => '§',
24
+ '&uml;' => '¨',
25
+ '&copy;' => '©',
26
+ '&ordf;' => 'ª',
27
+ '&laquo;' => '«',
28
+ '&not;' => '¬',
29
+ '&shy;' => '',
30
+ '&reg;' => '®',
31
+ '&macr;' => '¯',
32
+ '&deg;' => '°',
33
+ '&plusmn;' => '±',
34
+ '&sup2;' => '²',
35
+ '&sup3;' => '³',
36
+ '&acute;' => '´',
37
+ '&micro;' => 'µ',
38
+ '&para;' => '¶',
39
+ '&middot;' => '·',
40
+ '&cedil;' => '¸',
41
+ '&sup1;' => '¹',
42
+ '&ordm;' => 'º',
43
+ '&raquo;' => '»',
44
+ '&frac14;' => '¼',
45
+ '&frac12;' => '½',
46
+ '&frac34;' => '¾',
47
+ '&iquest;' => '¿',
48
+ '&times;' => '×',
49
+ '&divide;' => '÷',
50
+
51
+ # ISO 8859-1 Characters
52
+ '&Agrave;' => 'À',
53
+ '&Aacute;' => 'Á',
54
+ '&Acirc;' => 'Â',
55
+ '&Atilde;' => 'Ã',
56
+ '&Auml;' => 'Ä',
57
+ '&Aring;' => 'Å',
58
+ '&AElig;' => 'Æ',
59
+ '&Ccedil;' => 'Ç',
60
+ '&Egrave;' => 'È',
61
+ '&Eacute;' => 'É',
62
+ '&Ecirc;' => 'Ê',
63
+ '&Euml;' => 'Ë',
64
+ '&Igrave;' => 'Ì',
65
+ '&Iacute;' => 'Í',
66
+ '&Icirc;' => 'Î',
67
+ '&Iuml;' => 'Ï',
68
+ '&ETH;' => 'Ð',
69
+ '&Ntilde;' => 'Ñ',
70
+ '&Ograve;' => 'Ò',
71
+ '&Oacute;' => 'Ó',
72
+ '&Ocirc;' => 'Ô',
73
+ '&Otilde;' => 'Õ',
74
+ '&Ouml;' => 'Ö',
75
+ '&Oslash;' => 'Ø',
76
+ '&Ugrave;' => 'Ù',
77
+ '&Uacute;' => 'Ú',
78
+ '&Ucirc;' => 'Û',
79
+ '&Uuml;' => 'Ü',
80
+ '&Yacute;' => 'Ý',
81
+ '&THORN;' => 'Þ',
82
+ '&szlig;' => 'ß',
83
+ '&agrave;' => 'à',
84
+ '&aacute;' => 'á',
85
+ '&acirc;' => 'â',
86
+ '&atilde;' => 'ã',
87
+ '&auml;' => 'ä',
88
+ '&aring;' => 'å',
89
+ '&aelig;' => 'æ',
90
+ '&ccedil;' => 'ç',
91
+ '&egrave;' => 'è',
92
+ '&eacute;' => 'é',
93
+ '&ecirc;' => 'ê',
94
+ '&euml;' => 'ë',
95
+ '&igrave;' => 'ì',
96
+ '&iacute;' => 'í',
97
+ '&icirc;' => 'î',
98
+ '&iuml;' => 'ï',
99
+ '&eth;' => 'ð',
100
+ '&ntilde;' => 'ñ',
101
+ '&ograve;' => 'ò',
102
+ '&oacute;' => 'ó',
103
+ '&ocirc;' => 'ô',
104
+ '&otilde;' => 'õ',
105
+ '&ouml;' => 'ö',
106
+ '&oslash;' => 'ø',
107
+ '&ugrave;' => 'ù',
108
+ '&uacute;' => 'ú',
109
+ '&ucirc;' => 'û',
110
+ '&uuml;' => 'ü',
111
+ '&yacute;' => 'ý',
112
+ '&thorn;' => 'þ',
113
+ '&yuml;' => 'ÿ',
114
+
115
+ # General HTML Symbols
116
+ '&OElig;' => 'Œ',
117
+ '&oelig;' => 'œ',
118
+ '&Scaron;' => 'Š',
119
+ '&scaron;' => 'š',
120
+ '&Yuml;' => 'Ÿ',
121
+ '&fnof;' => 'ƒ',
122
+ '&circ;' => 'ˆ',
123
+ '&tilde;' => '˜',
124
+ '&ensp;' => ' ',
125
+ '&emsp;' => ' ',
126
+ '&thinsp;' => ' ',
127
+ '&zwnj;' => '',
128
+ '&zwj;' => '',
129
+ '&lrm;' => '',
130
+ '&rlm;' => '',
131
+ '&ndash;' => '–',
132
+ '&mdash;' => '—',
133
+ '&lsquo;' => "\u2018",
134
+ '&rsquo;' => "\u2019",
135
+ '&sbquo;' => '‚',
136
+ '&ldquo;' => '"',
137
+ '&rdquo;' => '"',
138
+ '&bdquo;' => '„',
139
+ '&dagger;' => '†',
140
+ '&Dagger;' => '‡',
141
+ '&bull;' => '•',
142
+ '&hellip;' => '…',
143
+ '&permil;' => '‰',
144
+ '&prime;' => '′',
145
+ '&Prime;' => '″',
146
+ '&lsaquo;' => '‹',
147
+ '&rsaquo;' => '›',
148
+ '&oline;' => '‾',
149
+ '&euro;' => '€',
150
+ '&trade;' => '™',
151
+ '&larr;' => '←',
152
+ '&uarr;' => '↑',
153
+ '&rarr;' => '→',
154
+ '&darr;' => '↓',
155
+ '&harr;' => '↔',
156
+ '&crarr;' => '↵',
157
+ '&lceil;' => '⌈',
158
+ '&rceil;' => '⌉',
159
+ '&lfloor;' => '⌊',
160
+ '&rfloor;' => '⌋',
161
+ '&loz;' => '◊',
162
+ '&spades;' => '♠',
163
+ '&clubs;' => '♣',
164
+ '&hearts;' => '♥',
165
+ '&diams;' => '♦',
166
+
167
+ # Mathematical Symbols
168
+ '&forall;' => '∀',
169
+ '&part;' => '∂',
170
+ '&exist;' => '∃',
171
+ '&empty;' => '∅',
172
+ '&nabla;' => '∇',
173
+ '&isin;' => '∈',
174
+ '&notin;' => '∉',
175
+ '&ni;' => '∋',
176
+ '&prod;' => '∏',
177
+ '&sum;' => '∑',
178
+ '&minus;' => '−',
179
+ '&lowast;' => '∗',
180
+ '&radic;' => '√',
181
+ '&prop;' => '∝',
182
+ '&infin;' => '∞',
183
+ '&ang;' => '∠',
184
+ '&and;' => '∧',
185
+ '&or;' => '∨',
186
+ '&cap;' => '∩',
187
+ '&cup;' => '∪',
188
+ '&int;' => '∫',
189
+ '&there4;' => '∴',
190
+ '&sim;' => '∼',
191
+ '&cong;' => '≅',
192
+ '&asymp;' => '≈',
193
+ '&ne;' => '≠',
194
+ '&equiv;' => '≡',
195
+ '&le;' => '≤',
196
+ '&ge;' => '≥',
197
+ '&sub;' => '⊂',
198
+ '&sup;' => '⊃',
199
+ '&nsub;' => '⊄',
200
+ '&sube;' => '⊆',
201
+ '&supe;' => '⊇',
202
+ '&oplus;' => '⊕',
203
+ '&otimes;' => '⊗',
204
+ '&perp;' => '⊥',
205
+ '&sdot;' => '⋅',
206
+
207
+ # Greek Characters
208
+ '&Alpha;' => 'Α',
209
+ '&Beta;' => 'Β',
210
+ '&Gamma;' => 'Γ',
211
+ '&Delta;' => 'Δ',
212
+ '&Epsilon;' => 'Ε',
213
+ '&Zeta;' => 'Ζ',
214
+ '&Eta;' => 'Η',
215
+ '&Theta;' => 'Θ',
216
+ '&Iota;' => 'Ι',
217
+ '&Kappa;' => 'Κ',
218
+ '&Lambda;' => 'Λ',
219
+ '&Mu;' => 'Μ',
220
+ '&Nu;' => 'Ν',
221
+ '&Xi;' => 'Ξ',
222
+ '&Omicron;' => 'Ο',
223
+ '&Pi;' => 'Π',
224
+ '&Rho;' => 'Ρ',
225
+ '&Sigma;' => 'Σ',
226
+ '&Tau;' => 'Τ',
227
+ '&Upsilon;' => 'Υ',
228
+ '&Phi;' => 'Φ',
229
+ '&Chi;' => 'Χ',
230
+ '&Psi;' => 'Ψ',
231
+ '&Omega;' => 'Ω',
232
+ '&alpha;' => 'α',
233
+ '&beta;' => 'β',
234
+ '&gamma;' => 'γ',
235
+ '&delta;' => 'δ',
236
+ '&epsilon;' => 'ε',
237
+ '&zeta;' => 'ζ',
238
+ '&eta;' => 'η',
239
+ '&theta;' => 'θ',
240
+ '&iota;' => 'ι',
241
+ '&kappa;' => 'κ',
242
+ '&lambda;' => 'λ',
243
+ '&mu;' => 'μ',
244
+ '&nu;' => 'ν',
245
+ '&xi;' => 'ξ',
246
+ '&omicron;' => 'ο',
247
+ '&pi;' => 'π',
248
+ '&rho;' => 'ρ',
249
+ '&sigmaf;' => 'ς',
250
+ '&sigma;' => 'σ',
251
+ '&tau;' => 'τ',
252
+ '&upsilon;' => 'υ',
253
+ '&phi;' => 'φ',
254
+ '&chi;' => 'χ',
255
+ '&psi;' => 'ψ',
256
+ '&omega;' => 'ω',
257
+ '&thetasym;' => 'ϑ',
258
+ '&upsih;' => 'ϒ',
259
+ '&piv;' => 'ϖ',
260
+ }
261
+
262
+ SPECIAL_HTML_CHARACTERS_REGEX = /&[a-zA-Z]+[0-9]*;/
263
+
264
+ def initialize(include_links=true)
265
+ @parsed_text = ''
266
+ @discard = false
267
+ @current_element = ''
268
+ @include_links = include_links
269
+
270
+ @current_list_index = -1
271
+ @lists = []
272
+
273
+
274
+ @table_columns = 0
275
+ @parsing_link = false
276
+ @link_text = ""
277
+ @link_href = ""
278
+ @parsing_bolds_and_italics = []
279
+
280
+ end
281
+
282
+ def convert_special_characters(text)
283
+
284
+ special_characters = text.scan(SPECIAL_HTML_CHARACTERS_REGEX).uniq
285
+
286
+ if special_characters.any?
287
+ special_characters.each do |special_character|
288
+ if HTML_SPECIAL_CHARACTERS.key?(special_character)
289
+ text.gsub!(special_character, HTML_SPECIAL_CHARACTERS[special_character])
290
+ elsif HTML_SPECIAL_ESCAPED_CHARACTERS.key?(special_character)
291
+ text.gsub!(special_character, "\\#{HTML_SPECIAL_ESCAPED_CHARACTERS[special_character]}")
292
+ end
293
+ end
294
+ end
295
+
296
+ text
297
+ end
298
+
299
+ def add_to_parsed_text(text)
300
+ if @parsing_link
301
+ @link_text << text
302
+ else
303
+ @parsed_text << convert_special_characters(text)
304
+ end
305
+ end
306
+
307
+ def start_element(name)
308
+
309
+ if name == :b || name == :strong
310
+ add_to_parsed_text("**")
311
+ @parsing_bolds_and_italics << :b
312
+ elsif name == :i || name == :em
313
+ add_to_parsed_text("*")
314
+ @parsing_bolds_and_italics << :i
315
+ end
316
+
317
+ if name == :script || name == :style || name == :head || name == :meta || name == :title
318
+ @discard = true
319
+ else
320
+ @discard = false
321
+ end
322
+
323
+ @current_element = name
324
+
325
+ if name== :hr
326
+ add_to_parsed_text("\n-------------------------------------------------------------------------------------------------------------------------------------------------\n")
327
+ end
328
+ if name == :ul || name == :ol
329
+ add_to_parsed_text("\n")
330
+ @lists << {
331
+ list_type: name,
332
+ ordered_list_level: 1,
333
+ ordered_list_type: "1", #If its an unordered list, this is not used.
334
+ }
335
+ end
336
+
337
+ if name.match(/h[1-6]/)
338
+ heading_number = name.to_s[1].to_i
339
+ add_to_parsed_text(("#" * heading_number) + " ")
340
+ end
341
+
342
+ if name == :a && @include_links
343
+ @parsing_link = true
344
+ @link_text = ""
345
+ @link_href = ""
346
+ end
347
+ end
348
+
349
+ def end_element(name)
350
+ @discard = false
351
+
352
+ if name == :p || name == :div || name.match(/h[1-6]/) || name == :br || name == :hr || name == :table || name == :tr
353
+ add_to_parsed_text("\n")
354
+ end
355
+
356
+ if name == :a && @include_links
357
+ @parsing_link = false
358
+
359
+ if @link_text.present? && @link_href.present?
360
+ add_to_parsed_text("[#{@link_text}](#{@link_href})")
361
+ elsif @link_href.present?
362
+ add_to_parsed_text("<#{@link_href}>")
363
+ elsif @link_text.present?
364
+ add_to_parsed_text(@link_text)
365
+ end
366
+ @link_text = ""
367
+ @link_href = ""
368
+ end
369
+
370
+ if name == :li
371
+ add_to_parsed_text("\n")
372
+ if @lists.last.present? && @lists.last[:list_type] == :ol
373
+ @lists.last[:ordered_list_level] += 1
374
+ end
375
+ end
376
+
377
+ if name == :ul || name == :ol
378
+ @lists.pop
379
+ end
380
+
381
+ if name == :b || name == :strong
382
+ #Find the first non space character backwards to check if the bold markers are empty or not.
383
+ last_non_space_index = @parsed_text.rindex(/\S/)
384
+ if last_non_space_index && last_non_space_index > 2 && @parsed_text[(last_non_space_index - 1)..last_non_space_index] == "**"
385
+ # Remove the empty bold markers from the parsed text.
386
+ @parsed_text = @parsed_text[0..(last_non_space_index - 2)]
387
+ else
388
+ #if last char is a space, we move the closing bold marker before the space to avoid having markdown syntax separated by spaces,
389
+ #which can cause issues in some markdown renderers. Same for italics.
390
+ if @parsed_text[-1] == " "
391
+ @parsed_text.rstrip!
392
+ add_to_parsed_text("**")
393
+ @parsed_text << " "
394
+ else
395
+ add_to_parsed_text("**")
396
+ end
397
+ end
398
+
399
+ elsif name == :i || name == :em
400
+ #Find the first non space character backwards to check if the italic markers are empty
401
+ last_non_space_index = @parsed_text.rindex(/\S/)
402
+ if last_non_space_index && last_non_space_index > 2 && @parsed_text[last_non_space_index] == "*"
403
+ # Remove the empty italic markers from the parsed text.
404
+ @parsed_text = @parsed_text[0..(last_non_space_index - 1)]
405
+ else
406
+ if @parsed_text[-1] == " "
407
+ @parsed_text.rstrip!
408
+ add_to_parsed_text("*")
409
+ @parsed_text << " "
410
+ else
411
+ add_to_parsed_text("*")
412
+ end
413
+ end
414
+ end
415
+ end
416
+
417
+ def attr(name, value)
418
+ if (@include_links && @current_element == :a && name == :href)
419
+ @link_href << "#{value}"
420
+ end
421
+
422
+ if (@current_element == :div && name == :style && value.match?(/border-\w*:\s*solid/))
423
+ add_to_parsed_text("\n-------------------------------------------------------------------------------------------------------------------------------------------------\n")
424
+ end
425
+
426
+ if (@current_element == :ol && name == :start && @lists.last.present?)
427
+ @lists.last[:ordered_list_level] = value.to_i
428
+ end
429
+ if (@current_element == :ol && name == :type && @lists.last.present?)
430
+ @lists.last[:ordered_list_type] = value
431
+ end
432
+ end
433
+
434
+ def attrs_done()
435
+ if @current_element == :li
436
+ current_list = @lists.last
437
+ if current_list.nil?
438
+ #sometimes we encounter a <li> outside of a list, so we just ignore it
439
+ return
440
+ end
441
+ if @lists.size > 1
442
+ # We are inside a nested list, so we need to indent
443
+ add_to_parsed_text(" " * 2 * (@lists.size - 1))
444
+ end
445
+
446
+ if current_list[:list_type] == :ol
447
+ if current_list[:ordered_list_type] == "A"
448
+ add_to_parsed_text("#{('A'.ord + current_list[:ordered_list_level] - 1).chr}. ")
449
+ elsif current_list[:ordered_list_type] == "a"
450
+ add_to_parsed_text("#{('a'.ord + current_list[:ordered_list_level] - 1).chr}. ")
451
+ elsif current_list[:ordered_list_type] == "I"
452
+ add_to_parsed_text("#{roman_numeral(current_list[:ordered_list_level])}. ")
453
+ elsif current_list[:ordered_list_type] == "i"
454
+ add_to_parsed_text("#{roman_numeral(current_list[:ordered_list_level]).downcase}. ")
455
+ else
456
+ add_to_parsed_text("#{current_list[:ordered_list_level]}. ")
457
+ end
458
+ else
459
+ add_to_parsed_text("* ")
460
+ end
461
+ end
462
+
463
+ end
464
+
465
+ def text(value)
466
+
467
+ unless value.blank? || @discard
468
+ if value.include?("-----------------------------")
469
+ add_to_parsed_text("\n-------------------------------------------------------------------------------------------------------------------------------------------------\n")
470
+ else
471
+ add_to_parsed_text("#{value}")
472
+ end
473
+ end
474
+ end
475
+
476
+ def postprocess
477
+
478
+ # remove linefeeds (\r\n and \r -> \n)
479
+ @parsed_text.gsub!(/\r\n?/, "\n")
480
+
481
+ # strip extra spaces
482
+ @parsed_text.gsub!(/[ \t]*\302\240+[ \t]*/, " ") # non-breaking spaces -> spaces
483
+ @parsed_text.gsub!(/\n[\t]+/, "\n") # space at start of lines
484
+ @parsed_text.gsub!(/[ \t]+\n/, "\n") # space at end of lines
485
+
486
+ # no more than two consecutive newlines
487
+ @parsed_text.gsub!(/[\n]{3,}/, "\n\n")
488
+
489
+ @parsed_text.strip
490
+ end
491
+
492
+ def roman_numeral(number)
493
+ roman_mapping = {
494
+ 1000 => "M", 900 => "CM", 500 => "D", 400 => "CD", 100 => "C",
495
+ 90 => "XC", 50 => "L", 40 => "XL", 10 => "X", 9 => "IX",
496
+ 5 => "V", 4 => "IV", 1 => "I"
497
+ }
498
+ result = ""
499
+ roman_mapping.each do |value, letter|
500
+ result << letter * (number / value)
501
+ number = number % value
502
+ end
503
+ result
504
+ end
505
+ end
506
+ end
@@ -97,6 +97,13 @@ module EmailSignatureParser
97
97
  MEETING_DOMAINS.any? { |regex| email_body =~ regex }
98
98
  end
99
99
 
100
+ def remove_bold_and_header_markings(text)
101
+ return "" if text.nil? || text.empty?
102
+ text = text.gsub("*", "") # remove bold markings
103
+ text = text.gsub(/^#+\s*/, "") # remove header markings
104
+ text
105
+ end
106
+
100
107
  def get_signature_sentences(name, email_address, email_body)
101
108
 
102
109
  if email_body.nil? || email_body.empty?
@@ -383,7 +390,7 @@ module EmailSignatureParser
383
390
  #Replace multiple commas with a single comma. or a comma preceded by a space
384
391
  address_text = address_text.gsub(/\s+/, ' ').gsub(/,+|\ ,/, ',')
385
392
 
386
- return address_text.strip
393
+ return remove_bold_and_header_markings(address_text).strip
387
394
  end
388
395
 
389
396
  def get_phones
@@ -541,6 +548,8 @@ module EmailSignatureParser
541
548
  next
542
549
  end
543
550
 
551
+ text = remove_bold_and_header_markings(text)
552
+
544
553
  sections = text.split(',')
545
554
 
546
555
  sections.each do |section|
@@ -619,7 +628,7 @@ module EmailSignatureParser
619
628
  end
620
629
  end
621
630
  if job_titles_found.size > 0
622
- parsed_titles << section.gsub(/\s+/, ' ').strip
631
+ parsed_titles << section.gsub(/\s+/, ' ').gsub('*', '').strip
623
632
  end
624
633
  end
625
634
  end
@@ -659,8 +668,10 @@ module EmailSignatureParser
659
668
  end
660
669
  end
661
670
 
671
+
672
+
662
673
  parsed_data = {
663
- name: parsed_name,
674
+ name: remove_bold_and_header_markings(parsed_name),
664
675
  email_address: email_address,
665
676
  address: address,
666
677
  phones: phones,
@@ -719,9 +730,10 @@ module EmailSignatureParser
719
730
  smart: true,
720
731
  mode: :generic,
721
732
  effort: :tolerant,
733
+ convert_special: false,
722
734
  }
723
735
  input = StringIO.new(content)
724
- text_handler = HtmlTextParser.new()
736
+ text_handler = HtmlToMarkdownParser.new()
725
737
 
726
738
  Ox.sax_html(text_handler, input, options)
727
739
  text_handler.postprocess
@@ -10,7 +10,7 @@ require 'yaml'
10
10
  require 'cgi'
11
11
 
12
12
  require_relative "email_signature_parser/parser"
13
- require_relative "email_signature_parser/html_text_parser"
13
+ require_relative "email_signature_parser/html_to_markdown_parser"
14
14
  require_relative "email_signature_parser/utils"
15
15
 
16
16
  module EmailSignatureParser
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: email_signature_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillermo Molini
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-10-21 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: ruby_postal
@@ -204,14 +203,13 @@ files:
204
203
  - lib/email_signature_parser/data/job_titles/acronyms.yaml
205
204
  - lib/email_signature_parser/data/job_titles/titles.yaml
206
205
  - lib/email_signature_parser/data/job_titles/titles_es.yaml
207
- - lib/email_signature_parser/html_text_parser.rb
206
+ - lib/email_signature_parser/html_to_markdown_parser.rb
208
207
  - lib/email_signature_parser/parser.rb
209
208
  - lib/email_signature_parser/utils.rb
210
209
  homepage: https://github.com/GMolini/email_signature_parser
211
210
  licenses:
212
211
  - MIT
213
212
  metadata: {}
214
- post_install_message:
215
213
  rdoc_options: []
216
214
  require_paths:
217
215
  - lib
@@ -226,8 +224,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
226
224
  - !ruby/object:Gem::Version
227
225
  version: '0'
228
226
  requirements: []
229
- rubygems_version: 3.4.10
230
- signing_key:
227
+ rubygems_version: 4.0.15
231
228
  specification_version: 4
232
229
  summary: Email Signature Parser
233
230
  test_files: []
@@ -1,181 +0,0 @@
1
- module EmailSignatureParser
2
- class HtmlTextParser < ::Ox::Sax
3
- attr_reader :parsed_text
4
-
5
- def initialize()
6
- @parsed_text = ''
7
- @discard = false
8
- @current_element = ''
9
-
10
- @current_list_index = -1
11
- @lists = []
12
-
13
-
14
- @table_columns = 0
15
- @parsing_link = false
16
- @link_text = ""
17
- @link_href = ""
18
- end
19
-
20
- def start_element(name)
21
-
22
- if name == :b || name == :strong
23
- @parsed_text << " "
24
- elsif name == :i || name == :em
25
- @parsed_text << " "
26
- end
27
-
28
- if name == :script || name == :style || name == :head || name == :meta || name == :title
29
- @discard = true
30
- else
31
- @discard = false
32
- end
33
-
34
- @current_element = name
35
- if name == :br
36
- @parsed_text << "\n"
37
- end
38
- if name== :hr
39
- @parsed_text << "\n-------------------------------------------------------------------------------------------------------------------------------------------------\n"
40
- end
41
- if name == :ul || name == :ol
42
- @parsed_text << "\n"
43
- @lists << {
44
- list_type: name,
45
- ordered_list_level: 1,
46
- ordered_list_type: "1", #If its an unordered list, this is not used.
47
- }
48
- end
49
-
50
- if name == :a
51
- @parsing_link = true
52
- @link_text = ""
53
- @link_href = ""
54
- end
55
- end
56
-
57
- def end_element(name)
58
- @discard = false
59
-
60
- if name == :b || name == :strong
61
- @parsed_text << " "
62
- elsif name == :i || name == :em
63
- @parsed_text << " "
64
- end
65
-
66
- if name == :p || name == :div || name.match(/h[1-6]/) || name == :br || name == :hr || name == :table || name == :tr
67
- @parsed_text << "\n"
68
- end
69
-
70
- if name == :a
71
- @parsed_text << "<a href=\"#{@link_href}\">#{@link_text}</a>" #leave links in html format
72
- @parsing_link = false
73
- @link_text = ""
74
- @link_href = ""
75
- end
76
-
77
- if name == :li
78
- @parsed_text << "\n"
79
- if @lists.last && @lists.last[:list_type] == :ol
80
- @lists.last[:ordered_list_level] += 1
81
- end
82
- end
83
-
84
- if name == :ul || name == :ol
85
- @lists.pop
86
- end
87
- end
88
-
89
- def attr(name, value)
90
-
91
- if (@current_element == :a && name == :href)
92
- @link_href << value
93
- end
94
-
95
- if (@current_element == :div && name == :style && value.match?(/border-\w*:\s*solid/))
96
- @parsed_text << "\n-------------------------------------------------------------------------------------------------------------------------------------------------\n"
97
- end
98
-
99
- if (@current_element == :ol && name == :start && !@lists.last.nil?)
100
- @lists.last[:ordered_list_level] = value.to_i
101
- end
102
- if (@current_element == :ol && name == :type && !@lists.last.nil?)
103
- @lists.last[:ordered_list_type] = value
104
- end
105
- end
106
-
107
- def attrs_done()
108
- if @current_element == :li
109
- current_list = @lists.last
110
- if current_list.nil?
111
- #sometimes we encounter a <li> outside of a list, so we just ignore it
112
- return
113
- end
114
- if @lists.size > 1
115
- # We are inside a nested list, so we need to indent
116
- @parsed_text << " " * 2 * (@lists.size - 1)
117
- end
118
-
119
- if current_list[:list_type] == :ol
120
- if current_list[:ordered_list_type] == "A"
121
- @parsed_text << "#{('A'.ord + current_list[:ordered_list_level] - 1).chr}. "
122
- elsif current_list[:ordered_list_type] == "a"
123
- @parsed_text << "#{('a'.ord + current_list[:ordered_list_level] - 1).chr}. "
124
- elsif current_list[:ordered_list_type] == "I"
125
- @parsed_text << "#{roman_numeral(current_list[:ordered_list_level])}. "
126
- elsif current_list[:ordered_list_type] == "i"
127
- @parsed_text << "#{roman_numeral(current_list[:ordered_list_level]).downcase}. "
128
- else
129
- @parsed_text << "#{current_list[:ordered_list_level]}. "
130
- end
131
- else
132
- @parsed_text << "* "
133
- end
134
- end
135
-
136
- end
137
-
138
- def text(value)
139
- if @parsing_link
140
- @link_text << value
141
- return
142
- end
143
- unless value.nil? || value.empty? || @discard
144
- if value.include?("-----------------------------")
145
- @parsed_text << "\n-------------------------------------------------------------------------------------------------------------------------------------------------\n"
146
- else
147
- @parsed_text << value
148
- end
149
- end
150
- end
151
-
152
- def postprocess()
153
-
154
- # remove linefeeds (\r\n and \r -> \n)
155
- @parsed_text.gsub!(/\r\n?/, "\n")
156
-
157
- # strip extra spaces
158
- @parsed_text.gsub!(/[ \t]*\302\240+[ \t]*/, " ") # non-breaking spaces -> spaces
159
- @parsed_text.gsub!(/\n[\t]+/, "\n") # space at start of lines
160
- @parsed_text.gsub!(/[ \t]+\n/, "\n") # space at end of lines
161
-
162
-
163
- @parsed_text = @parsed_text.split("\n").map(&:strip).join("\n") # strip lines
164
-
165
- end
166
-
167
- def roman_numeral(number)
168
- roman_mapping = {
169
- 1000 => "M", 900 => "CM", 500 => "D", 400 => "CD", 100 => "C",
170
- 90 => "XC", 50 => "L", 40 => "XL", 10 => "X", 9 => "IX",
171
- 5 => "V", 4 => "IV", 1 => "I"
172
- }
173
- result = ""
174
- roman_mapping.each do |value, letter|
175
- result << letter * (number / value)
176
- number = number % value
177
- end
178
- result
179
- end
180
- end
181
- end