htmlGen.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. ########################################################################
  2. # Searx-Qt - Lightweight desktop application for Searx.
  3. # Copyright (C) 2020-2022 CYBERDEViL
  4. #
  5. # This file is part of Searx-Qt.
  6. #
  7. # Searx-Qt is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Searx-Qt is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. ########################################################################
  21. import html
  22. from urllib.parse import quote
  23. from searxqt.core.images import ImagesSettings
  24. from searxqt.utils.string import formatFileSize, formatFileCount
  25. from searxqt.translations import _
  26. class HtmlGen:
  27. def wrapHtml(bodyStr, styleStr=""):
  28. return f"""
  29. <html>
  30. <head>
  31. <style>{styleStr}</style>
  32. </head>
  33. <body>
  34. {bodyStr}
  35. </body>
  36. </html>"""
  37. class FailedResponsesHtml:
  38. def create(results, message, css):
  39. """
  40. @param result: List with failed search result object (searxqt.core.requests.Result).
  41. @type result: list
  42. @param message: TODO
  43. @param css: Optionaly set a stylesheet, it should be the content only;
  44. so do not include the <style> tags!
  45. @type css: str
  46. """
  47. body = f"<p id=\"top-message\">{message}</p>" \
  48. f"<h3>{_('Failed requests')}:</h3>" \
  49. "<table class=\"fail\">"
  50. lastResult = results[-1]
  51. for result in results:
  52. url = result.url()
  53. error = html.escape(result.error())
  54. content = html.escape(result.text())
  55. body += "<tr>" \
  56. "<td colspan=\"2\" class=\"fail-instance\">" \
  57. f"<a href=\"{url}\">{url}</a>" \
  58. "</td></tr>" \
  59. "<tr>" \
  60. f"<td class=\"fail-prefix\">{_('Error')}</td>" \
  61. f"<td class=\"fail-value\">{error}</td>" \
  62. "</tr>" \
  63. "<tr>" \
  64. f"<td class=\"fail-prefix\">{_('Content')}</td>" \
  65. f"<td class=\"fail-value\">{content}</td>" \
  66. "</tr>"
  67. if result != lastResult:
  68. body += "<tr><td colspan=\"2\"><hr></td></tr>"
  69. body += "</table>"
  70. return HtmlGen.wrapHtml(body, css)
  71. class ResultsHtml:
  72. """! Create HTML from a valid search response.
  73. """
  74. def create(jsonResult, css):
  75. """
  76. @param jsonResult: The josn result of a search response.
  77. @type jsonResult: json or dict
  78. @param css: Optionaly set a stylesheet, it should be the content only;
  79. so do not include the <style> tags!
  80. @type css: str
  81. """
  82. elemStr = ""
  83. # Search query suggestions
  84. data = jsonResult.get('suggestions', [])
  85. if data:
  86. elemStr += ResultsHtml.createHrefListElement(
  87. 'search', _('Suggestions'), data
  88. )
  89. elemStr += "<hr>"
  90. # Search query corrections
  91. data = jsonResult.get('corrections', [])
  92. if data:
  93. elemStr += ResultsHtml.createHrefListElement(
  94. 'search', _('Corrections'), data
  95. )
  96. elemStr += "<hr>"
  97. # Unresponsive engines
  98. data = jsonResult.get('unresponsive_engines', [])
  99. if data:
  100. elemStr += ResultsHtml.createUnresponsiveEnginesElement(data)
  101. elemStr += "<hr>"
  102. # Infoboxes
  103. data = jsonResult.get('infoboxes', [])
  104. if data:
  105. elemStr += "<p id=\"infoboxes_header\">Infoboxes</p>"
  106. for infoboxData in data:
  107. elemStr += ResultsHtml.createInfobox(infoboxData)
  108. # Answers
  109. data = jsonResult.get('answers', [])
  110. if data:
  111. elemStr += ResultsHtml.createStrListElement(_('Answers'), data)
  112. elemStr += "<hr>"
  113. # First 'normal' results
  114. data = jsonResult.get('results', [])
  115. for resultData in data:
  116. if resultData.get('category') == 'images':
  117. continue
  118. elemStr += ResultsHtml.createResultElement(resultData)
  119. elemStr += "<hr>"
  120. # Images results
  121. for resultData in data:
  122. if resultData.get('category') != 'images':
  123. continue
  124. elemStr += ResultsHtml.createImageResultElement(resultData)
  125. return HtmlGen.wrapHtml(elemStr, css)
  126. def createStrListElement(title, data):
  127. elemStr = " | ".join([html.escape(entry) for entry in data])
  128. return ResultsHtml.createListContainer(title, elemStr)
  129. def createHrefListElement(scheme, title, data):
  130. elemStr = ""
  131. scheme += ":"
  132. for entry in data:
  133. if elemStr:
  134. elemStr += " | "
  135. url = scheme + quote(entry)
  136. elemStr += f"<a href=\"{url}\">{html.escape(entry)}</a>"
  137. return ResultsHtml.createListContainer(title, elemStr)
  138. def createListContainer(title, elems):
  139. return f"<div class=\"{title}\"><b>{title}</b>: {elems}</div>"
  140. def createUnresponsiveEnginesElement(data):
  141. elemStr = ""
  142. for name, error in data:
  143. if elemStr:
  144. elemStr += " | "
  145. elemStr += f"{html.escape(name)} <i>({html.escape(error)})</i>"
  146. return ResultsHtml.createListContainer(_('Unresponsive engines'), elemStr)
  147. def createInfobox(infoboxData):
  148. # Create new infobox
  149. elemStr = "<div class=\"infobox\">"
  150. # Title
  151. data = html.escape(infoboxData.get("infobox", "-"))
  152. elemStr += "<center><div class=\"infobox_title\">" \
  153. f"{data}</div></center>"
  154. # ID
  155. data = infoboxData.get("id", None)
  156. if data:
  157. data = html.escape(data)
  158. elemStr += "<center><div class=\"infobox_id\">" \
  159. f"<a href=\"{data}\">{data}</a></div></center>"
  160. # Image
  161. if ImagesSettings.enabled:
  162. data = infoboxData.get("img_src", None)
  163. if data:
  164. data = html.escape(data)
  165. elemStr += f"<center><img class=\"infobox_img\" src=\"{data}\"/></center>"
  166. # Content
  167. data = infoboxData.get("content", None)
  168. if data:
  169. data = html.escape(data)
  170. elemStr += f"<div class=\"infobox_content\">{data}</div>"
  171. # Attributes
  172. data = infoboxData.get("attributes", None)
  173. if data:
  174. elemStr += "<p class=\"infobox_attr_head\">Attributes</p>"
  175. elemStr += "<table class=\"infobox_attr_table\">"
  176. for attributeData in data:
  177. # TODO for now skip images ..
  178. if "value" not in attributeData:
  179. continue
  180. # New row
  181. elemStr += "<tr>"
  182. value = html.escape(attributeData.get('label', ''))
  183. elemStr += f"<td class=\"infobox_label\">{value}</td>"
  184. value = html.escape(attributeData.get('value', ''))
  185. elemStr += f"<td class=\"infobox_value\">{value}</td>"
  186. elemStr += "</tr>"
  187. elemStr += "</table>"
  188. # Urls list
  189. data = infoboxData.get("urls", None)
  190. if data:
  191. elemStr += "<p class=\"infobox_links_head\">Links</p>"
  192. elemStr += "<ul class=\"infobox_link_list\">"
  193. for urlData in data:
  194. url = html.escape(urlData.get("url"))
  195. title = html.escape(urlData.get("title", "-"))
  196. elemStr += "<li class=\"infobox_link_list_item\">" \
  197. f"<a href=\"{url}\" title=\"{url}\">{title}</a>" \
  198. "</li>"
  199. elemStr += "</ul>"
  200. # Engines
  201. data = infoboxData.get("engines", None)
  202. if data:
  203. elemStr += ResultsHtml.createStrListElement(_('Engines'), data)
  204. # Closing tag of the new infobox element.
  205. elemStr += "</div>"
  206. elemStr += "<hr>"
  207. return elemStr
  208. def createImageResultElement(data):
  209. thumbnail_src=html.escape(data.get('thumbnail_src', ''))
  210. imgElem = ""
  211. if ImagesSettings.enabled:
  212. imgElem = f"""<a href="thumb://{thumbnail_src}"><img class="result-thumbnail" src="{thumbnail_src}"/></a>"""
  213. else:
  214. imgElem = f"""<a href="thumb://{thumbnail_src}">Thumb</a>"""
  215. # The given width and height are used for minimum width and height,
  216. # it will stretch when the image is larger. The image should not be
  217. # larger then this so we get nice even rows and columns.
  218. width = ImagesSettings.maxWidth + 26 # FIXME 26 are margings from the
  219. # table/tr/td.
  220. height = ImagesSettings.maxWidth + 26
  221. # Wrap inside a single row/column table as a HACK so we can have
  222. # floating elements and control their style a little better (padding,
  223. # background etc.).
  224. elemStr = f"""<table class="thumbnail" width={width} height={height}>
  225. <tr><td><center>{imgElem}</center></td></tr></table>"""
  226. return elemStr;
  227. def createResultElement(data):
  228. # Create general elements
  229. title=html.escape(data.get('title', ''))
  230. url=html.escape(data.get('url', ''))
  231. content=html.escape(data.get('content', ''))
  232. engine = ''
  233. for e in data.get('engines', []):
  234. engine += f"{e} "
  235. engine = engine.rstrip()
  236. # Wrap inside a single row/column table as a HACK so we can have
  237. # floating elements and control their style a little better (padding,
  238. # background etc.).
  239. elemStr = """<table class="result"><tr><td>"""
  240. elemStr += "<div class=\"results\">" \
  241. f"<h4 class=\"result-title\"><i>{engine}: </i>" \
  242. f"<a href=\"{url}\">{title}</a></h4>" \
  243. "<div style=\"margin-left: 10px;\">" \
  244. f"<p class=\"result-description\">{content}</p>" \
  245. f"<p class=\"result-url\">{url}</p>"
  246. # Add file data elements
  247. elemStr += ResultsHtml.createFileSection(data)
  248. elemStr += "</div></div></tr></td></table>"
  249. return elemStr
  250. def createFileSection(data):
  251. elemStr = ""
  252. value = data.get('magnetlink', '')
  253. if value:
  254. value = html.escape(value)
  255. elemStr += f"<a href=\"{value}\">Magnet</a> "
  256. value = data.get('torrentfile', '')
  257. if value:
  258. value = html.escape(value)
  259. elemStr += f"<a href=\"{value}\">Torrent</a> "
  260. value = data.get('filesize', 0)
  261. if value:
  262. elemStr += formatFileSize(value) + " "
  263. value = data.get('files', 0)
  264. if value:
  265. elemStr += formatFileCount(value) + " "
  266. value = data.get('seed', None)
  267. if value is not None:
  268. value = html.escape(str(value))
  269. elemStr += f"seeders: {value} "
  270. value = data.get('leech', None)
  271. if value is not None:
  272. value = html.escape(str(value))
  273. elemStr += f"leechers: {value} "
  274. value = data.get('img_format', "")
  275. if value:
  276. value = html.escape(value)
  277. elemStr += f"format: {value} "
  278. if elemStr:
  279. elemStr = "<p class=\"result-file\">" + elemStr + "</p>"
  280. return elemStr