html2doc 1.5.3 → 1.5.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/html2doc/base.rb +17 -94
- data/lib/html2doc/mime.rb +91 -15
- data/lib/html2doc/version.rb +1 -1
- data/lib/html2doc/xml.rb +83 -0
- data/lib/html2doc.rb +1 -0
- 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: 46856bf56ad5dd95f8f5781dc11049bb4600060c28c49715a262837ece8028bf
|
4
|
+
data.tar.gz: 866ba19867f233b45aeee436df719679623d671902b30476b61952f7a6357e1f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b949f47c356437ce418f65ce7fd1c497648d0d0e960fe1e05d7318d280ddf6de23ddad8e5ab94a18f447b3eaba948b2a4db69ca2d00f0dcfebd692933a64c1da
|
7
|
+
data.tar.gz: 953999bd39aa1c1b6a0e1a34c939dcbdba01242c898f5a587eab546b4c9a4578e8051c9c68b49900390fcb3ab5d2de4ad6f0f1bce1af5a8ee6e7bd4daf300966
|
data/lib/html2doc/base.rb
CHANGED
@@ -30,8 +30,7 @@ class Html2Doc
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def process_header(headerfile)
|
33
|
-
|
34
|
-
|
33
|
+
headerfile.nil? and return
|
35
34
|
doc = File.read(headerfile, encoding: "utf-8")
|
36
35
|
doc = header_image_cleanup(doc, @dir1, @filename,
|
37
36
|
File.dirname(@filename))
|
@@ -66,6 +65,7 @@ class Html2Doc
|
|
66
65
|
end
|
67
66
|
|
68
67
|
def cleanup(docxml)
|
68
|
+
locate_landscape(docxml)
|
69
69
|
namespace(docxml.root)
|
70
70
|
image_cleanup(docxml, @dir1, @imagedir)
|
71
71
|
mathml_to_ooml(docxml)
|
@@ -76,76 +76,11 @@ class Html2Doc
|
|
76
76
|
docxml
|
77
77
|
end
|
78
78
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
<body> </body> </html>
|
85
|
-
HERE
|
86
|
-
|
87
|
-
def to_xhtml(xml)
|
88
|
-
xml.gsub!(/<\?xml[^>]*>/, "")
|
89
|
-
unless /<!DOCTYPE /.match? xml
|
90
|
-
xml = '<!DOCTYPE html SYSTEM
|
91
|
-
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' + xml
|
92
|
-
end
|
93
|
-
xml = xml.gsub(/<!--\s*\[([^\]]+)\]>/, "<!-- MSWORD-COMMENT \\1 -->")
|
94
|
-
.gsub(/<!\s*\[endif\]\s*-->/, "<!-- MSWORD-COMMENT-END -->")
|
95
|
-
Nokogiri::XML.parse(xml)
|
96
|
-
end
|
97
|
-
|
98
|
-
DOCTYPE = <<~"DOCTYPE".freeze
|
99
|
-
<!DOCTYPE html SYSTEM "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
100
|
-
DOCTYPE
|
101
|
-
|
102
|
-
def from_xhtml(xml)
|
103
|
-
xml.to_xml.sub(%{ xmlns="http://www.w3.org/1999/xhtml"}, "")
|
104
|
-
.sub(DOCTYPE, "").gsub(%{ />}, "/>")
|
105
|
-
.gsub(/<!-- MSWORD-COMMENT (.+?) -->/, "<!--[\\1]>")
|
106
|
-
.gsub(/<!-- MSWORD-COMMENT-END -->/, "<![endif]-->")
|
107
|
-
.gsub("\n-->\n", "\n-->\n")
|
108
|
-
end
|
109
|
-
|
110
|
-
def msword_fix(doc)
|
111
|
-
# brain damage in MSWord parser
|
112
|
-
doc.gsub!(%r{<w:DoNotOptimizeForBrowser></w:DoNotOptimizeForBrowser>},
|
113
|
-
"<w:DoNotOptimizeForBrowser/>")
|
114
|
-
doc.gsub!(%r{<span style="mso-special-character:footnote"/>},
|
115
|
-
'<span style="mso-special-character:footnote"></span>')
|
116
|
-
doc.gsub!(%r{<div style="mso-element:footnote-list"></div>},
|
117
|
-
'<div style="mso-element:footnote-list"/>')
|
118
|
-
doc.gsub!(%r{(<a style="mso-comment-reference:[^>/]+)/>}, "\\1></a>")
|
119
|
-
doc.gsub!(%r{<link rel="File-List"}, "<link rel=File-List")
|
120
|
-
doc.gsub!(%r{<meta http-equiv="Content-Type"},
|
121
|
-
"<meta http-equiv=Content-Type")
|
122
|
-
doc.gsub!(%r{></m:jc>}, "/>")
|
123
|
-
doc.gsub!(%r{></v:stroke>}, "/>")
|
124
|
-
doc.gsub!(%r{></v:f>}, "/>")
|
125
|
-
doc.gsub!(%r{></v:path>}, "/>")
|
126
|
-
doc.gsub!(%r{></o:lock>}, "/>")
|
127
|
-
doc.gsub!(%r{></v:imagedata>}, "/>")
|
128
|
-
doc.gsub!(%r{></w:wrap>}, "/>")
|
129
|
-
doc.gsub!(%r{<(/)?m:(span|em)\b}, "<\\1\\2")
|
130
|
-
doc.gsub!(%r{&tab;|&tab;},
|
131
|
-
'<span style="mso-tab-count:1">  </span>')
|
132
|
-
doc.split(%r{(<m:oMath>|</m:oMath>)}).each_slice(4).map do |a|
|
133
|
-
a.size > 2 and a[2] = a[2].gsub(/>\s+</, "><")
|
134
|
-
a
|
135
|
-
end.join
|
136
|
-
end
|
137
|
-
|
138
|
-
PRINT_VIEW = <<~XML.freeze
|
139
|
-
|
140
|
-
<xml>
|
141
|
-
<w:WordDocument>
|
142
|
-
<w:View>Print</w:View>
|
143
|
-
<w:Zoom>100</w:Zoom>
|
144
|
-
<w:DoNotOptimizeForBrowser/>
|
145
|
-
</w:WordDocument>
|
146
|
-
</xml>
|
147
|
-
<meta http-equiv='Content-Type' content="text/html; charset=utf-8"/>
|
148
|
-
XML
|
79
|
+
def locate_landscape(_docxml)
|
80
|
+
css = read_stylesheet(@stylesheet)
|
81
|
+
@landscape = css.scan(/div\.\S+\s+\{\s*page:\s*[^;]+L;\s*\}/m)
|
82
|
+
.map { |e| e.sub(/^div\.(\S+).*$/m, "\\1") }
|
83
|
+
end
|
149
84
|
|
150
85
|
def define_head1(docxml, _dir)
|
151
86
|
docxml.xpath("//*[local-name() = 'head']").each do |h|
|
@@ -168,18 +103,21 @@ class Html2Doc
|
|
168
103
|
end
|
169
104
|
|
170
105
|
def stylesheet(_filename, _header_filename, cssname)
|
171
|
-
|
172
|
-
cssname = File.join(File.dirname(__FILE__), "wordstyle.css")
|
173
|
-
stylesheet = File.read(cssname, encoding: "UTF-8")
|
106
|
+
stylesheet = read_stylesheet(cssname)
|
174
107
|
xml = Nokogiri::XML("<style/>")
|
175
108
|
# s = Nokogiri::XML::CDATA.new(xml, "\n#{stylesheet}\n")
|
176
109
|
# xml.children.first << Nokogiri::XML::Comment.new(xml, s)
|
177
110
|
xml.children.first << Nokogiri::XML::CDATA
|
178
111
|
.new(xml, "\n<!--\n#{stylesheet}\n-->\n")
|
179
|
-
|
180
112
|
xml.root.to_s
|
181
113
|
end
|
182
114
|
|
115
|
+
def read_stylesheet(cssname)
|
116
|
+
(cssname.nil? || cssname.empty?) and
|
117
|
+
cssname = File.join(File.dirname(__FILE__), "wordstyle.css")
|
118
|
+
File.read(cssname, encoding: "UTF-8")
|
119
|
+
end
|
120
|
+
|
183
121
|
def define_head(docxml)
|
184
122
|
title = docxml.at("//*[local-name() = 'head']/*[local-name() = 'title']")
|
185
123
|
head = docxml.at("//*[local-name() = 'head']")
|
@@ -195,30 +133,15 @@ class Html2Doc
|
|
195
133
|
head.add_child css
|
196
134
|
elsif title.nil?
|
197
135
|
head.children.first.add_previous_sibling css
|
198
|
-
else
|
199
|
-
title.add_next_sibling css
|
136
|
+
else title.add_next_sibling css
|
200
137
|
end
|
201
138
|
end
|
202
139
|
|
203
|
-
def namespace(root)
|
204
|
-
{
|
205
|
-
o: "urn:schemas-microsoft-com:office:office",
|
206
|
-
w: "urn:schemas-microsoft-com:office:word",
|
207
|
-
v: "urn:schemas-microsoft-com:vml",
|
208
|
-
m: "http://schemas.microsoft.com/office/2004/12/omml",
|
209
|
-
}.each { |k, v| root.add_namespace_definition(k.to_s, v) }
|
210
|
-
end
|
211
|
-
|
212
|
-
def rootnamespace(root)
|
213
|
-
root.add_namespace(nil, "http://www.w3.org/TR/REC-html40")
|
214
|
-
end
|
215
|
-
|
216
140
|
def bookmarks(docxml)
|
217
141
|
docxml.xpath("//*[@id][not(@name)][not(@style = 'mso-element:footnote')]")
|
218
142
|
.each do |x|
|
219
|
-
|
220
|
-
%w(shapetype v:shapetype shape v:shape).include?(x.name)
|
221
|
-
|
143
|
+
(x["id"].empty? ||
|
144
|
+
%w(shapetype v:shapetype shape v:shape).include?(x.name)) and next
|
222
145
|
if x.children.empty? then x.add_child("<a name='#{x['id']}'></a>")
|
223
146
|
else x.children.first.previous = "<a name='#{x['id']}'></a>"
|
224
147
|
end
|
data/lib/html2doc/mime.rb
CHANGED
@@ -76,11 +76,9 @@ class Html2Doc
|
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
79
|
-
# max width for Word document is 400, max height is 680
|
80
79
|
def image_resize(img, path, maxheight, maxwidth)
|
81
80
|
s, realsize = get_image_size(img, path)
|
82
|
-
|
83
|
-
|
81
|
+
s[0] == nil && s[1] == nil and return s
|
84
82
|
if img.name == "svg" && !img["viewBox"]
|
85
83
|
img["viewBox"] = "0 0 #{s[0]} #{s[1]}"
|
86
84
|
end
|
@@ -115,21 +113,100 @@ class Html2Doc
|
|
115
113
|
|
116
114
|
# only processes locally stored images
|
117
115
|
def image_cleanup(docxml, dir, localdir)
|
116
|
+
maxheight, maxwidth = page_dimensions(docxml)
|
118
117
|
docxml.traverse do |i|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
i["width"], i["height"] = image_resize(i, local_filename, 680, 400)
|
128
|
-
i["src"] = File.join(File.basename(dir), new_filename)
|
118
|
+
skip_image_cleanup?(i) and next
|
119
|
+
local_filename = rename_image(i, dir, localdir)
|
120
|
+
i["width"], i["height"] =
|
121
|
+
if landscape?(i)
|
122
|
+
image_resize(i, local_filename, maxwidth, maxheight)
|
123
|
+
else
|
124
|
+
image_resize(i, local_filename, maxheight, maxwidth)
|
125
|
+
end
|
129
126
|
end
|
130
127
|
docxml
|
131
128
|
end
|
132
129
|
|
130
|
+
def landscape?(img)
|
131
|
+
img.ancestors.each do |a|
|
132
|
+
a.name == "div" or next
|
133
|
+
@landscape.include?(a["class"]) and return true
|
134
|
+
end
|
135
|
+
false
|
136
|
+
end
|
137
|
+
|
138
|
+
def rename_image(img, dir, localdir)
|
139
|
+
local_filename = localname(img["src"], localdir)
|
140
|
+
new_filename = "#{mkuuid}#{File.extname(img['src'])}"
|
141
|
+
FileUtils.cp local_filename, File.join(dir, new_filename)
|
142
|
+
img["src"] = File.join(File.basename(dir), new_filename)
|
143
|
+
local_filename
|
144
|
+
end
|
145
|
+
|
146
|
+
def skip_image_cleanup?(img)
|
147
|
+
src = img["src"]
|
148
|
+
(img.element? && %w(img v:imagedata).include?(img.name)) or return true
|
149
|
+
(src.nil? || src.empty? || /^http/.match?(src) ||
|
150
|
+
%r{^data:(image|application)/[^;]+;base64}.match?(src)) and return true
|
151
|
+
false
|
152
|
+
end
|
153
|
+
|
154
|
+
# we are going to use the 2nd instance of @page in the Word CSS,
|
155
|
+
# skipping the cover page. Currently doesn't deal with Landscape.
|
156
|
+
# Scan both @stylesheet and docxml.to_xml (where @standardstylesheet has ended up)
|
157
|
+
# Allow 0.9 * height to fit caption
|
158
|
+
def page_dimensions(docxml)
|
159
|
+
stylesheet = read_stylesheet(@stylesheet)
|
160
|
+
page_size = find_page_size_in_doc(stylesheet, docxml.to_xml) or
|
161
|
+
return [680, 400]
|
162
|
+
m_size = /size:\s*(\S+)\s+(\S+)\s*;/.match(page_size) or return [680, 400]
|
163
|
+
m_marg = /margin:\s*(\S+)\s+(\S+)\s*(\S+)\s*(\S+)\s*;/.match(page_size) or
|
164
|
+
return [680, 400]
|
165
|
+
[0.9 * (units_to_px(m_size[2]) - units_to_px(m_marg[1]) - units_to_px(m_marg[3])),
|
166
|
+
units_to_px(m_size[1]) - units_to_px(m_marg[2]) - units_to_px(m_marg[4])]
|
167
|
+
rescue StandardError
|
168
|
+
[680, 400]
|
169
|
+
end
|
170
|
+
|
171
|
+
def find_page_size_in_doc(stylesheet, doc)
|
172
|
+
find_page_size(stylesheet, "WordSection2", false) ||
|
173
|
+
find_page_size(stylesheet, "WordSection3", false) ||
|
174
|
+
find_page_size(doc, "WordSection2", true) ||
|
175
|
+
find_page_size(doc, "WordSection3", true) ||
|
176
|
+
find_page_size(stylesheet, "", false) || find_page_size(doc, "", true)
|
177
|
+
end
|
178
|
+
|
179
|
+
# if in_xml, CSS is embedded in XML <style> tag
|
180
|
+
def find_page_size(stylesheet, klass, in_xml)
|
181
|
+
xml_found = false
|
182
|
+
found = false
|
183
|
+
ret = ""
|
184
|
+
stylesheet&.lines&.each do |l|
|
185
|
+
in_xml && l.include?("<style") and xml_found = true and found = false
|
186
|
+
in_xml && l.include?("</style>") and xml_found = false
|
187
|
+
/^\s*@page\s+#{klass}/.match?(l) and found = true
|
188
|
+
found && /^\s*\{?size:/.match?(l) and ret += l
|
189
|
+
found && /^\s*\{?margin:/.match?(l) and ret += l
|
190
|
+
if found && /}/.match?(l)
|
191
|
+
!ret.blank? && (!in_xml || xml_found) and return ret
|
192
|
+
ret = ""
|
193
|
+
found = false
|
194
|
+
end
|
195
|
+
end
|
196
|
+
nil
|
197
|
+
end
|
198
|
+
|
199
|
+
def units_to_px(measure)
|
200
|
+
m = /^(\S+)(pt|cm)/.match(measure)
|
201
|
+
ret = case m[2]
|
202
|
+
when "px" then (m[1].to_f * 0.75)
|
203
|
+
when "pt" then m[1].to_f
|
204
|
+
when "cm" then (m[1].to_f * 28.346456693)
|
205
|
+
when "in" then (m[1].to_f * 72)
|
206
|
+
end
|
207
|
+
ret.to_i
|
208
|
+
end
|
209
|
+
|
133
210
|
# do not parse the header through Nokogiri, since it will contain
|
134
211
|
# non-XML like <![if !supportFootnotes]>
|
135
212
|
def header_image_cleanup(doc, dir, filename, localdir)
|
@@ -155,8 +232,7 @@ class Html2Doc
|
|
155
232
|
f.write %{<xml xmlns:o="urn:schemas-microsoft-com:office:office">
|
156
233
|
<o:MainFile HRef="../#{filename}.htm"/>}
|
157
234
|
Dir.entries(dir).sort.each do |item|
|
158
|
-
|
159
|
-
|
235
|
+
(item == "." || item == ".." || /^\./.match(item)) and next
|
160
236
|
f.write %{ <o:File HRef="#{item}"/>\n}
|
161
237
|
end
|
162
238
|
f.write("</xml>\n")
|
data/lib/html2doc/version.rb
CHANGED
data/lib/html2doc/xml.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
class Html2Doc
|
2
|
+
NOKOHEAD = <<~HERE.freeze
|
3
|
+
<!DOCTYPE html SYSTEM
|
4
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
5
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
6
|
+
<head> <title></title> <meta charset="UTF-8" /> </head>
|
7
|
+
<body> </body> </html>
|
8
|
+
HERE
|
9
|
+
|
10
|
+
def to_xhtml(xml)
|
11
|
+
xml.gsub!(/<\?xml[^>]*>/, "")
|
12
|
+
unless /<!DOCTYPE /.match? xml
|
13
|
+
xml = '<!DOCTYPE html SYSTEM
|
14
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' + xml
|
15
|
+
end
|
16
|
+
xml = xml.gsub(/<!--\s*\[([^\]]+)\]>/, "<!-- MSWORD-COMMENT \\1 -->")
|
17
|
+
.gsub(/<!\s*\[endif\]\s*-->/, "<!-- MSWORD-COMMENT-END -->")
|
18
|
+
Nokogiri::XML.parse(xml)
|
19
|
+
end
|
20
|
+
|
21
|
+
DOCTYPE = <<~DOCTYPE.freeze
|
22
|
+
<!DOCTYPE html SYSTEM "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
23
|
+
DOCTYPE
|
24
|
+
|
25
|
+
def from_xhtml(xml)
|
26
|
+
xml.to_xml.sub(%{ xmlns="http://www.w3.org/1999/xhtml"}, "")
|
27
|
+
.sub(DOCTYPE, "").gsub(%{ />}, "/>")
|
28
|
+
.gsub(/<!-- MSWORD-COMMENT (.+?) -->/, "<!--[\\1]>")
|
29
|
+
.gsub(/<!-- MSWORD-COMMENT-END -->/, "<![endif]-->")
|
30
|
+
.gsub("\n-->\n", "\n-->\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
def msword_fix(doc)
|
34
|
+
# brain damage in MSWord parser
|
35
|
+
doc.gsub!(%r{<w:DoNotOptimizeForBrowser></w:DoNotOptimizeForBrowser>},
|
36
|
+
"<w:DoNotOptimizeForBrowser/>")
|
37
|
+
doc.gsub!(%r{<span style="mso-special-character:footnote"/>},
|
38
|
+
'<span style="mso-special-character:footnote"></span>')
|
39
|
+
doc.gsub!(%r{<div style="mso-element:footnote-list"></div>},
|
40
|
+
'<div style="mso-element:footnote-list"/>')
|
41
|
+
doc.gsub!(%r{(<a style="mso-comment-reference:[^>/]+)/>}, "\\1></a>")
|
42
|
+
doc.gsub!(%r{<link rel="File-List"}, "<link rel=File-List")
|
43
|
+
doc.gsub!(%r{<meta http-equiv="Content-Type"},
|
44
|
+
"<meta http-equiv=Content-Type")
|
45
|
+
doc.gsub!(%r{></m:jc>}, "/>")
|
46
|
+
doc.gsub!(%r{></v:stroke>}, "/>")
|
47
|
+
doc.gsub!(%r{></v:f>}, "/>")
|
48
|
+
doc.gsub!(%r{></v:path>}, "/>")
|
49
|
+
doc.gsub!(%r{></o:lock>}, "/>")
|
50
|
+
doc.gsub!(%r{></v:imagedata>}, "/>")
|
51
|
+
doc.gsub!(%r{></w:wrap>}, "/>")
|
52
|
+
doc.gsub!(%r{<(/)?m:(span|em)\b}, "<\\1\\2")
|
53
|
+
doc.gsub!(%r{&tab;|&tab;},
|
54
|
+
'<span style="mso-tab-count:1">  </span>')
|
55
|
+
doc.split(%r{(<m:oMath>|</m:oMath>)}).each_slice(4).map do |a|
|
56
|
+
a.size > 2 and a[2] = a[2].gsub(/>\s+</, "><")
|
57
|
+
a
|
58
|
+
end.join
|
59
|
+
end
|
60
|
+
|
61
|
+
PRINT_VIEW = <<~XML.freeze
|
62
|
+
|
63
|
+
<xml>
|
64
|
+
<w:WordDocument>
|
65
|
+
<w:View>Print</w:View>
|
66
|
+
<w:Zoom>100</w:Zoom>
|
67
|
+
<w:DoNotOptimizeForBrowser/>
|
68
|
+
</w:WordDocument>
|
69
|
+
</xml>
|
70
|
+
<meta http-equiv='Content-Type' content="text/html; charset=utf-8"/>
|
71
|
+
XML
|
72
|
+
|
73
|
+
def namespace(root)
|
74
|
+
{ o: "urn:schemas-microsoft-com:office:office",
|
75
|
+
w: "urn:schemas-microsoft-com:office:word",
|
76
|
+
v: "urn:schemas-microsoft-com:vml",
|
77
|
+
m: "http://schemas.microsoft.com/office/2004/12/omml" }.each { |k, v| root.add_namespace_definition(k.to_s, v) }
|
78
|
+
end
|
79
|
+
|
80
|
+
def rootnamespace(root)
|
81
|
+
root.add_namespace(nil, "http://www.w3.org/TR/REC-html40")
|
82
|
+
end
|
83
|
+
end
|
data/lib/html2doc.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: html2doc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.5.
|
4
|
+
version: 1.5.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ribose Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-06-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: htmlentities
|
@@ -293,6 +293,7 @@ files:
|
|
293
293
|
- lib/html2doc/notes.rb
|
294
294
|
- lib/html2doc/version.rb
|
295
295
|
- lib/html2doc/wordstyle.css
|
296
|
+
- lib/html2doc/xml.rb
|
296
297
|
homepage: https://github.com/metanorma/html2doc
|
297
298
|
licenses:
|
298
299
|
- CC-BY-SA-3.0
|